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

---

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

### 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` is derived from `NEXTAUTH_URL` and **already includes the instance path** (e.g., `/instance2`), so you don't need to append it separately.

```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 object with all ticket field names and values |

**Examples:**

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

```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: Buffer,                 // Binary content as Buffer
  contentType: "application/json", // MIME type
  fileInfo: {
    ID: 42,                        // File ID (use with DeleteUpvf)
    FileName: "drawing.upvf",
    DateTimeCreated: Date
  }
}
```

**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?, ...params)` | Generates a PDF report for the current ticket |

**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 the standard `fetch` API, which is available globally in the Node.js execution environment.

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

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

---