Custom Trigger System — Script Triggers Guide

Overview

The custom trigger system allows attaching logic to any command in the V7 ERP platform. Triggers can be either API triggers (calling external HTTP endpoints) or Script triggers (executing JavaScript in-process via the Jint engine).

Trigger Timing

Timing Behavior DB Access
Before Runs before the command handler. Use for validation. If it fails, the command is not executed. Read-only
After Runs after the command handler within the same transaction. If it fails, the entire transaction rolls back. Read + Write
Instead Replaces the command entirely (API only). The trigger response is returned as the command result. N/A
  • Instead is restricted to Kind = Api only. Scripts cannot use Instead timing.
  • After Script triggers are awaited (not fire-and-forget) and share the command’s transaction.
  • After API triggers are still fire-and-forget via the event queue.

Defining Triggers

Create a trigger via the API:

POST /api/CustomTrigger
{
  "id": "guid",
  "commandName": "CreateOrderCommand",
  "timing": 1,       // 1=Before, 2=After, 3=Instead
  "kind": 2,         // 1=Api, 2=Script
  "endPoint": null,   // Required for Api, must be null for Script
  "script": "if (!request.customerAccountId) reject('Customer is required', 'E001');",
  "order": 0,         // Execution order (lower runs first)
  "isActive": true,
  "description": "Validate customer is set"
}

Activating/Deactivating

Set isActive to false to disable a trigger without deleting it. Inactive triggers are excluded from execution.

Ordering Multiple Triggers

Multiple triggers for the same CommandName and Timing execute in ascending Order value. For Before triggers, the first failure stops the pipeline.

Writing Scripts

Available Context Variables

Variable Available In Description
request Before, After The command’s JSON payload as a JavaScript object
response After only The command handler’s response as a JavaScript object
db Before, After Database access helper
http Before, After HTTP client helper
result Before, After Result object: { isValid: true, errorMessage: null, errorCode: null }
reject(message, code) Before, After Function to mark the trigger as failed

Database Access (db)

Method Description
db.queryFirst(sql, params) Returns first row as object, or null
db.query(sql, params) Returns all rows as array of objects
db.scalar(sql, params) Returns a single scalar value

Parameters use positional binding with @p0, @p1, etc:

var customer = db.queryFirst(
  "SELECT Id, Name FROM Customers WHERE Id = @p0", 
  [request.customerAccountId]
);

HTTP Access (http)

Method Signature Description
http.get(url, headers?) (string, object?) → response Sends a GET request
http.post(url, body, headers?) (string, string, object?) → response Sends a POST request with a JSON body
http.put(url, body, headers?) (string, string, object?) → response Sends a PUT request with a JSON body

The response object has the following shape:

Property Type Description
status number HTTP status code (e.g. 200, 404)
ok boolean true when status is 2xx
body object | string Parsed JSON object if the response is JSON, otherwise raw string

The body parameter for post/put must be a JSON string — use JSON.stringify() to serialize a JS object.

// GET with optional headers
var result = http.get("https://api.example.com/items/1", {
  Authorization: "Bearer " + request.apiToken
});

if (!result.ok) {
  reject("External API error: " + result.status, "E_HTTP");
}

var item = result.body; // already parsed as JS object

// POST with a body
var payload = JSON.stringify({ orderId: response.id, amount: request.totalAmount });
http.post("https://webhook.example.com/notify", payload, {
  Authorization: "Bearer token",
  "X-Source": "V7-ERP"
});

Security note: Scripts run with the network permissions of the application server. Only configure HTTP triggers with trusted, internal endpoints to avoid SSRF vulnerabilities.

Before vs After Capabilities

  • Before scripts: DB is read-only. Write operations (INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, EXEC, EXECUTE) are rejected.
  • After scripts: DB is read+write. All operations are allowed within the command’s transaction.

Example: Before Validation Script

// Validate that customer account exists and has sufficient credit
var customer = db.queryFirst(
  "SELECT CreditLimit, CurrentBalance FROM CustomerAccounts WHERE Id = @p0",
  [request.customerAccountId]
);

if (!customer) {
  reject("Customer account not found", "E_CUST_404");
}

if (customer.CurrentBalance + request.totalAmount > customer.CreditLimit) {
  reject("Credit limit exceeded", "E_CREDIT_LIMIT");
}

Example: After Data Manipulation Script

// Create a stock reservation after order is created
db.query(
  "INSERT INTO StockReservations (OrderId, ItemId, Quantity, CreatedAt) VALUES (@p0, @p1, @p2, @p3)",
  [response.id, request.itemId, request.quantity, new Date().toISOString()]
);

Security Constraints

Constraint Value
Memory limit 4 MB
Execution timeout 5 seconds
Recursion limit 64 levels
Strict mode Enabled (undefined variable access throws)
.NET API access None — only injected variables are accessible

Scripts run in a sandboxed Jint engine with no access to the file system, network, or .NET types beyond the explicitly injected request, response, db, result, and reject.

JavaScript Language Reference

Scripts use the Jint interpreter which supports ES2020+ JavaScript syntax:

Note: Browser APIs (document, window, fetch, XMLHttpRequest) and Node.js APIs (require, fs, process) are not available.

Troubleshooting

Common Errors

Error Cause Fix
Script execution timed out Script ran longer than 5 seconds Optimize loops, reduce DB queries
Memory limit exceeded Script allocated more than 4 MB Reduce data processing, avoid large arrays
Read-only violation Before script attempted a write query Move write operations to an After trigger
ReferenceError: X is not defined Strict mode — used undefined variable Declare variables with var/let/const

Checking Error Logs

Script execution errors are logged in the TriggerExecutionErrorLog table with:

  • TriggerId: Which trigger failed
  • CommandName: Which command was being executed
  • RequestJson: The command payload
  • ErrorMessage: The error details
  • Timestamp: When it occurred

Disabling a Faulty Trigger

Set isActive = false via the Update API to immediately disable a trigger without deleting it:

PUT /api/CustomTrigger
{
  "id": "trigger-guid",
  "isActive": false,
  ...
}

Breaking Changes

  • Entity structure: CustomTrigger entity restructured — BeforeEndPoint, AfterEndPoint, InsteadEndPoint replaced with Timing, Kind, EndPoint, Script, Order, IsActive, Description
  • IRequestTrigger no longer required: All ICommandRequest<T> commands are now trigger-eligible automatically
  • API contracts: Create/Update trigger commands use new fields. Old field names are removed.
  • Cache strategy: Per-command distributed cache replaces single-key GetCustomTriggerQuery approach