# 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 #" + UniqueID,
    "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 | Checked Field (date field), Overdue (ms) |
| `onEmailReceived` | An inbound email is received | Subject Regex, Content Regex |

> **Note:** The **Interval** field is stored but not used. Scheduled checks run at fixed server
> intervals: overdue tickets every 5 minutes (`Worker:ListenerIntervalMinutes`), inbound email
> every minute (`Worker:EmailIngestionIntervalMinutes`).

> **Important — a listener runs only once per ticket.** After a listener has run for a ticket,
> it does not run for that ticket again, even if its condition becomes true again later (for
> example, the same step is accepted a second time). The run is recorded even if the custom code
> fails. `onEmailReceived` and `onInteractionEmailSent` listeners are the exception: they run for
> every matching email.

### 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: .*
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. 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)`
- Set ticket fields from values in the email (via `getRegexMatches()`)

**How it works internally:**

- A background job reads new emails from the support mailbox every minute (server setting
  `Worker:EmailIngestionIntervalMinutes`). The listener's **Interval** field is not used.
- Each email is linked to a ticket: the ticket named by `[#123]` at the end of the subject, else the
  ticket of the same email conversation, else a new ticket. The listener code runs for that ticket.
- An email that was already imported (same mail ID) is skipped, so it is never processed twice.
- Every enabled `onEmailReceived` listener is checked, independent of the ticket's workflow.
- An empty Subject Regex or Content Regex matches everything. An invalid pattern is compared as
  plain text.
- When matched, the listener's Custom Code executes with full access to the macro API. It runs
  without a user, so `createComment` does nothing (a server warning is logged).

**Email-specific helpers** (only in `onEmailReceived` and `onInteractionEmailSent` listeners):

| Function | Returns |
|---|---|
| `getEmailData()` | `{ from, subject, body }` of the received or sent email (`body` is the HTML body; for a sent email, `from` is the support mailbox). `null` in all other listener types |
| `getRegexMatches()` | Array of the capture groups from the Subject Regex match, then from the Content Regex match. `[]` in all other listener types |

```javascript
// Subject Regex: Order (\d+)
const e = getEmailData();
const m = getRegexMatches();
await setTicketValue("Order No", m[0]);
await sendMailTo(["orders@example.com"], "Mail from " + e.from, "Order " + m[0] + " received");
```

---

## 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.

> **Note — jumps in event listeners:** A listener has no step of its own, so the jump moves the
> ticket's **pending** step. A listener with a Workflow ID moves the pending step of that workflow.
> A Default listener moves the pending step only when the ticket has exactly one pending step;
> otherwise nothing moves. A failed script does not jump. `onTicketOverdue` and `onEmailReceived`
> listeners cannot jump — the request is only written to the server log.

### Staying on the Current Step

A jump to the step's **own** step number means "stay here". The step is not accepted or denied,
no new step is created, and the step stays pending. Use this when a precondition is not met yet.

```javascript
// Step 1: do not move on until the CTR discipline is filled in
const ctr = await getTicketValue("CTR Discipline First Action Owner");

if (!ctr) {
  await showPopup("Please fill in 'CTR Discipline First Action Owner' before approving.");
  await stepBackwardJumpTo(1); // this code runs on step 1 -> stay on step 1
  return false;
}

await stepForwardJumpTo(2);
return true;
```

> **Note:** The user sees no "Step approved" message when the step stays. Tell them what to do
> with [`showPopup`](#showpopupmessage) — otherwise the click appears to do nothing. On a `None`
> step a self-jump simply stops the automatic execution; the step waits.

---

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

#### showPopup(message)

Shows a popup with `message` to the user whose action ran the code. The popup opens after the
step has been processed — on the Approvals tab and on the Kanban board. For an automatic `None`
step the user is the one who attached the workflow, or who accepted/denied the step before it.

```javascript
// Tell the approver what happened next
await showPopup("The ticket was forwarded to the modelling discipline.");
return true;
```

**Example: Explain why the step did not move on**

```javascript
const description = await getTicketValue("Tag Description");

if (!description) {
  await showPopup("Add a Tag Description first. The step stays open.");
  await stepBackwardJumpTo(3); // this code runs on step 3 -> stay on step 3
  return false;
}
return true;
```

**Example: Several lines**

```javascript
// Each call adds a line to the same popup
await showPopup("Checks passed:");
await showPopup("- Tag Description present");
await showPopup("- CTR discipline: " + (await getTicketValue("CTR Discipline First Action Owner")));
return true;
```

**Behaviour:**

- Several calls are shown in **one** popup, one line each, in call order.
- The text is shown as plain text. HTML tags in the message are not rendered.
- Messages from a chain of automatic `None` steps are shown together with the message of the
  step the user clicked, in execution order.
- The popup needs a user in a browser. An event listener, or a `None` step run by the server
  without a user, has none: the message is written to the server log and not shown.
- **Compile** prints `showPopup: <message>` in the Compile Result instead of opening a popup.
- If the script fails, the error is shown instead and the popup is not.

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>#${UniqueID}</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(await createPdfReport("Template"));
attachments.push(await 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 |
| `${Status}` | `Open` | Ticket status (built-in property, not a ticket field) |
| `${Details["Field Name"]}` | `Broken pipe` | Any ticket field value (configured ticket fields only) |
| `${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 }` |
| `await getTicketComponents()` | Returns an **array** of the model components attached to the ticket, ordered by `ID`. Each entry is the full `Components` row — currently `{ ID, UniqueID, UID, CompName, BoundingBox, DrawingName, ComponentView, AssignedDate }`; new columns appear automatically. `ComponentView` is `"2D"` for P&ID components; anything else (including `null`) is 3D. |
| `await getTicketComponent(columnName)` | Returns an **array** with one value per component — the named column of each row from `getTicketComponents()`, in the same order. An unknown column name yields `null` for every component. |

**Examples:**

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

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

const components = await getTicketComponents();
const names3D = components.filter(c => c.ComponentView !== "2D").map(c => c.CompName);
console.log("3D components:", names3D.join(", "));

const drawings = await getTicketComponent("DrawingName");
console.log("Drawings:", drawings.filter(Boolean).join(", "));
```

> **Note:** `getTicketValue()` and `getPunchlistDetails()` read only the configured ticket
> fields. Built-in ticket properties such as `PunchItem` and `Status` are not ticket fields:
> `getTicketValue("PunchItem")` and `getTicketValue("Status")` return `null`. Use the `UniqueID`
> variable for the ticket number. (The server-side placeholders `${PunchItem}` and `${Status}`
> do work in `sendMailTo` message and subject text written in normal quotes, e.g.
> `'Ticket ${PunchItem} is ${Status}'` — see [String Replacements](#c-string-replacements). In a
> backtick string JavaScript evaluates `${…}` itself, where these names are not defined.)

> **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(await 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: UniqueID,
    stage: await getTicketValue("Stage"),
  }),
});
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: UniqueID,
    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). The step's own number = stay on the step |
| `await stepBackwardJumpTo(step, message?)` | Macros, Listeners | Jump backward (denial). The step's own number = stay on the step |
| `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 |
| `await showPopup(message)` | Macros | Show a popup to the user who accepted/denied the step — see [Show Message](#a-show-message). Ignored in listeners and auto-executed `None` steps |

### 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, `eval()`, `require()`, `process.`, prototype access, …)
  are rejected when a **field validator** is saved, with the pattern named in the error. Workflow
  step and listener code is checked only for its length (max 50,000 characters).

---