Custom Trigger System — Script Triggers Guide
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 = Apionly. 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 failedCommandName: Which command was being executedRequestJson: The command payloadErrorMessage: The error detailsTimestamp: 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:
CustomTriggerentity restructured —BeforeEndPoint,AfterEndPoint,InsteadEndPointreplaced withTiming,Kind,EndPoint,Script,Order,IsActive,Description IRequestTriggerno longer required: AllICommandRequest<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
GetCustomTriggerQueryapproach