# 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:**
>
> - [Workflows](#1-workflow-step-macros) &mdash; Settings > Workflows
> - [Event Listeners](#2-workflow-level-event-macros) &mdash; Settings > Listeners
> - [Field Validators](#field-validators-valuechangevalidating) &mdash; Settings > Workflows > Validators
> - [Custom Code Editor](#custom-code-editor) &mdash; Embedded in Workflows, Listeners, and Validators 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:**

1. Navigate to **Settings > Workflows**
2. Select a workflow from the list
3. Click the **Code** cell for the desired workflow step
4. Write JavaScript code in the integrated code editor
5. 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:**

```javascript
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](#2-workflow-level-event-macros) 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:**

1. Navigate to **Settings > Listeners**
2. Click the **Add (+)** button
3. Select a **Type** from the dropdown
4. Configure the required fields for that type
5. 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:**

1. Navigate to **Settings > Workflows > Validators**
2. Select a workflow from the dropdown
3. Click **Add** to create a new validator
4. Select one or more fields from the **Fields** TagBox
5. Enter an **Error Message** (shown to the user on failure)
6. Write validation code that returns `true` (allow) or `false` (block)

See [Field Validators](#field-validators-valuechangevalidating) 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:**

1. Navigate to **Settings > Listeners**
2. Add a listener with **Type** = `onEmailReceived`
3. Set **Subject Regex** (regex pattern to match the email subject)
4. Set **Content Regex** (regex pattern to match the email body)
5. Set **Interval** (cron expression for how often to poll, e.g., `*/5 * * * *` for every 5 minutes)
6. 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**

```javascript
// 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**

```javascript
// 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:**

```javascript
// 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.

```javascript
// 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.

```javascript
// 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.

> **Note:** The message is posted to the activity chat under the acting user once the jump has
> advanced. A script running without a user (an auto-executed `None` step triggered by attaching
> the workflow, or by the Worker) has no author for the comment — the message is written to the
> server log instead.

---

## Refuse to Modify a Value

Field validators can prevent a value from being saved by returning `false`.

**How it works:**

1. User changes a field value
2. All active validators for that field execute in order
3. If any validator returns `false`, the change is **blocked**
4. The configured **Error Message** is shown as a toast notification
5. The field retains its previous value

**Example: Prevent unauthorized changes**

```javascript
// 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**

```javascript
// 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:**

1. Navigate to **Settings > Workflows > Validators**
2. Create or edit a validator
3. Set the **Error Message** field (max 500 characters)
4. 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

```javascript
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

```javascript
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:

```javascript
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

```javascript
const report = await createPdfReport("ReportTemplate");
await sendMailTo(
  ["user@example.com"],
  "Please see the attached report.",
  "Monthly Report",
  report
);
```

### With UPVF File Attachment

```javascript
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)

```javascript
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](#c-string-replacements) for the full list of supported variables.

> **Environment behavior:** In production runs the mail is sent through the configured mail
> service. The Compile/Test button never sends — it prints the recipients and the fully expanded
> subject and body into the Compile Result output instead, which is where an unresolved `${…}`
> shows up.

---

## 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`; `App__BaseUrl`/`KEYCLOAK_REDIRECT_URL` on the .NET stack) |
| `${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) |

### Date Format Patterns

| 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:

```javascript
// 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"));
```

### FormatDate and ParseFormatDate

These helper functions are available in custom code for formatting arbitrary dates:

```javascript
// 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` **already includes the instance path** (e.g., `/instance2`), so you don't need to append it separately. It is derived from `NEXTAUTH_URL`, and on the .NET stack from `App__BaseUrl` (falling back to `KEYCLOAK_REDIRECT_URL`) plus `BASE_PATH` — so a script written for either stack builds the same URL.

```javascript
// 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:

```html
<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 **array** of value rows: `{ PunchlistDetails: { Detail }, Value, PunchDetailID }` |

**Examples:**

```javascript
const tagNo = await getTicketValue("Tag No");
console.log("Tag Number:", tagNo);

const rows = await getPunchlistDetails();
const status = rows.find(r => r.PunchlistDetails.Detail === "Status")?.Value;
console.log("Status:", status);
```

> **Note:** `getPunchlistDetails()` returns an array of rows, not a name→value object — for a
> single field, `getTicketValue()` is the simpler call. (Only field *validators* get a name→value
> object, via `getAllFields()`.)

### Writing Ticket Data

| Function | Description |
|---|---|
| `await setTicketValue(fieldName, value)` | Writes a field's value (creates the value if unset; **throws** when the field itself does not exist) |
| `await updatePunchlistDetails(data)` | Batch-updates multiple fields (an unknown field name is skipped with a server-side warning) |
| `await createHiddenField(data)` | Writes a field's value and, when the field does not exist yet, **creates it as a hidden field** first |

**Examples:**

```javascript
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:

```javascript
{
  filename: "drawing.upvf",        // File name
  content: Uint8Array,             // File bytes (decoded lazily on first access)
  contentType: "application/json", // MIME type
  fileInfo: {
    ID: 42,                        // File ID (use with DeleteUpvf)
    FileName: "drawing.upvf",
    DateTimeCreated: Date
  }
}
```

> **Note:** `content` is a `Uint8Array` (the old Node executor returned a `Buffer`);
> `content.length` and indexed access work the same. When the object is only pushed into
> `attachments` for `sendMailTo`, the bytes are attached server-side and `content` is never
> decoded.

**Examples:**

```javascript
// 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?)` | Renders the stored ticket report template for the current ticket and returns an attachment handle |

How it works on the .NET stack:

- The **stored pdfme report template** (Administration → Tickets → Ticket Report) is rendered
  server-side against the same `reportData.*` values the Report button uses — one page, with the
  ticket's `Initial`/`Final` snapshots and workflow signatures. The `templateName` argument is
  ignored: there is only one stored template (the old executor ignored it too).
- The renderer draws the same field subset as the Report button (text, multiVariableText, image,
  line, rectangle — anything else renders blank) and supports the `#` alias suffix and an embedded
  base PDF.
- The return value is an attachment handle `{ filename, contentType, __attachmentRef }` — pass it
  to `sendMailTo` (directly or via `attachments`). The PDF bytes stay server-side.
- **No template configured** renders the same placeholder page the Report button shows ("Ask your
  admin for a valid template configuration.").
- **On a rendering error** the function returns `null` (logged server-side);
  `sendMailTo(..., report)` then sends the mail without the attachment instead of failing the
  script.

**Example:**

```javascript
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)

```javascript
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 a `fetch` helper shaped like the standard API.

> **Allowlisted.** Custom code runs with the server's network position, so `fetch` is **disabled
> until an administrator lists allowed hosts** in the `CustomCode:Fetch` configuration
> (`CustomCode__Fetch__AllowedHosts__0=api.example.com`, or `.example.com` to include subdomains).
> Private/loopback targets additionally require `AllowPrivateNetworks`; every redirect hop is
> re-validated. A rejected request throws a catchable JS error. The old Node executor exposed the
> global `fetch` with no restrictions — see `REQUIRED_DOCS/EVENT_LISTENERS.md`.

### GET Request

```javascript
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
```

### POST Request

```javascript
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);
```

### POST with Custom Headers

```javascript
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

```javascript
// 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 **and covered by the `CustomCode:Fetch` allowlist** (private/internal addresses
> additionally need `AllowPrivateNetworks`).

---

# 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:**

```javascript
// Ensure uppercase only
return getCurrentFieldValue() === getCurrentFieldValue().toUpperCase();
```

**Role-based restriction:**

```javascript
// Only admins can set to "Critical"
if (getCurrentFieldValue() === "Critical") {
  return hasRole("Admin");
}
return true;
```

**Cross-field validation (select both fields in the Fields TagBox):**

```javascript
// 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(templateName?)` | Macros, Listeners | Render the stored report template; returns an attachment handle (`templateName` is ignored — the template is a singleton) |
| `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.) — allowlisted, see [HTTP(S) POSTs and GETs](#f-https-posts-and-gets) |

### Available Objects

| Object | Context | Description |
|---|---|---|
| `userAction.Result` | Macros | The step action result: `ActionResult.Yes` (accepted), `ActionResult.No` (denied), `ActionResult.Undefined` (auto-executed `None` step) |
| `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` (.NET stack: `App__BaseUrl`/`KEYCLOAK_REDIRECT_URL` + `BASE_PATH`), **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 — only items produced by `extractUpvf`/`createPdfReport` are attached; hand-built objects are ignored |

## Execution Environment

Custom code runs server-side in a sandboxed JavaScript interpreter (Jint), not in Node.js:

- **Limits:** 30 s execution timeout, 64 MB memory, 256 call-stack depth. Exceeding one fails the
  script with a plain-language error.
- **No Node globals:** `Buffer`, `process`, `require`, `setTimeout`/`setInterval` and the rest of
  the Node API do not exist. Standard JavaScript built-ins (`JSON`, `Math`, `Date`, typed arrays,
  `Promise`, …) are available.
- **Errors are catchable:** a failing helper (`fetch` rejection, unknown field in
  `setTicketValue`, …) throws a normal JS error, so `try { … } catch (e) { … }` works as
  documented.
- **Prohibited patterns** (SQL keywords, `require()`, `process.`, prototype access, …) are
  rejected when the code is **saved**, with the pattern named in the error.

---