Custom (Script) Rules
When a preset can't express the logic you need, write a rule in JavaScript. Custom rules run inside a secure sandbox: you have field data and a set of helper functions, but no filesystem, no network (unless explicitly allowed), and no Node.js APIs.
Every preset generates code against this same public API, so you can start from a preset, switch it to custom code, and edit from there.
Scripting API Reference
All function and variable names in the scripting API use snake_case.
Common Functions (All Levels)
These functions are available in field-level, document-level, and transaction-level rules.
set_error(message)
Set the rule result to error. The message is displayed in the Transaction Viewer.
set_error('Invoice number is missing');
For field-level rules, the bound field name is automatically shown as a clickable tag before the message in the Transaction Viewer, so you don't need to include the field name in the message text. Write just the error description:
// Good - field name tag is added automatically by the UI
set_error('Value is required');
// Avoid - "Invoice Date: " prefix would be redundant
set_error('Invoice Date: Value is required');
set_warning(message)
Set the rule result to warning.
set_warning('Amount seems unusually high');
set_success(message)
Set the rule result to success (optional, since rules that finish without calling set_error or set_warning are considered successful).
set_success('All checks passed');
normalize(text, options)
Apply text transformations. Returns the transformed string.
| Option | Type | Effect |
|---|---|---|
trim | boolean | Remove leading/trailing whitespace |
lowercase | boolean | Convert to lowercase |
uppercase | boolean | Convert to uppercase |
removeSpaces | boolean | Collapse all whitespace runs to a single space |
removeSpecialChars | boolean | Strip everything except letters, digits, and spaces |
const clean = normalize(value, { trim: true, uppercase: true });
fetch(url, options) (requires HTTP allowlist)
Make an HTTP request. Only works when the project's HTTP allowlist is enabled and the target domain is listed. Supports standard fetch options (method, headers, body). Returns { ok, status, data }.
const res = await fetch('https://api.example.com/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: value }),
});
if (res.ok) { /* ... */ }
Transaction & document metadata
Rules can read and write arbitrary key/value metadata attached to the parent transaction or document. This is how you carry reference data into a rule (PO catalogues, expected suppliers, policy flags), and how a rule can persist enrichment back to the transaction so later steps (or other rules) see it.
Two ways to read metadata are available: the bound objects and the helper functions:
// Direct read via the bound objects
const expectedVendor = transaction_metadata['vendor'];
const taxId = document_metadata['tax_id'];
// Or via the helpers (no arg = whole object, with arg = single key)
const allTxnMd = get_transaction_metadata();
const country = get_document_metadata('origin_country');
To write metadata you must go through the setters: direct assignment to the bound objects is scratch-space only and is discarded when the rule finishes:
// Persists into transaction.metadata after the validation run completes
set_transaction_metadata('verified_vendor', 'ACME-001');
// Persists into the parent document's metadata
set_document_metadata('match_quality', 'high');
Transaction-level rules see metadata for every document, so the document helpers take an explicit document id or 0-based index:
const inv = get_document('Invoice');
const country = get_document_metadata(inv.document_id, 'origin_country');
set_document_metadata(inv.document_id, 'verified', true);
set_document_metadata(0, 'verified', true); // same thing, by index
| Function | Available at | Description |
|---|---|---|
get_transaction_metadata(key?) | field, document, transaction | Read transaction.metadata. No arg returns the whole object. |
set_transaction_metadata(key, value) | field, document, transaction | Write a key into transaction.metadata. Persisted after the run. |
get_document_metadata(key?) | field, document | Read the parent document's metadata. |
set_document_metadata(key, value) | field, document | Write a key into the parent document's metadata. |
get_document_metadata(doc_id_or_index, key?) | transaction | Read a specific document's metadata. |
set_document_metadata(doc_id_or_index, key, value) | transaction | Write a key into a specific document's metadata. |
The metadata-driven presets use exactly this API under the hood.
Return-value pattern
Instead of (or in addition to) calling set_error / set_warning, you can return an object:
return { valid: false, message: 'Value out of range', severity: 'error' };
// severity: 'error' (default) or 'warning'
return { valid: true, message: 'Looks good' };
For field-level rules you can also return a modified value:
return { valid: true, modified_value: value.trim() };
Clickable Field & Document Tags
In set_error() or set_warning() messages, you can embed {field:FieldName} and {document:DocType} tags. These render as clickable badges in the Transaction Viewer:
| Tag | Appearance | Click action |
|---|---|---|
{field:Invoice Number} | Blue badge | Selects the field in the Data Panel |
{document:Passport} | Purple badge | Navigates to the document |
This is especially useful in document-level and transaction-level rules where the error involves multiple fields or documents:
// Document-level: reference specific fields
highlight_fields('Invoice Number', 'Total Amount');
set_error('{field:Invoice Number} does not match {field:Total Amount}');
// Transaction-level: reference documents
highlight_fields('Passport Number');
set_error('Mismatch between {document:Application} and {document:Passport}');
For field-level rules, the bound field is automatically rendered as a clickable tag, so you don't need {field:...} tags in field-level messages.
Field-Level API
Field-level rules operate on a single field value. Use them for format checks, normalization, and single-value validation.
Context Variables
| Variable | Type | Description |
|---|---|---|
value | any | Current field value |
field_name | string | Display name of the field |
field_id | string | Stable field identifier (survives renames) |
field_type | string | Data type (string, number, date, etc.) |
field_settings | object | Field-specific settings from project configuration |
document_id | string | Parent document ID |
document_class_id | string | Parent document class ID |
transaction_metadata | object | Read view of transaction.metadata (also via get_transaction_metadata()) |
document_metadata | object | Read view of the parent document's metadata (also via get_document_metadata()) |
set_value(new_value)
Replace the field value. Primarily used in normalization rules.
// Normalize a phone number
const digits = value.replace(/\D/g, '');
if (digits.length === 10) {
set_value(`(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`);
set_success('Phone number formatted');
}
add_suggestion(value, label?, source?)
Suggest an alternative value for the current field. Suggestions appear as a dropdown in the Data Panel.
if (value !== 'USD' && value !== 'EUR') {
add_suggestion('USD', 'US Dollar', 'Currency Rule');
add_suggestion('EUR', 'Euro', 'Currency Rule');
set_warning('Currency should be USD or EUR');
}
Full Example: Normalize SSN
if (value) {
const digits = value.replace(/\D/g, '');
if (digits.length === 9) {
set_value(`${digits.slice(0,3)}-${digits.slice(3,5)}-${digits.slice(5)}`);
set_success('SSN normalized');
} else {
set_error('SSN must be 9 digits');
}
}
Document-Level API
Document-level rules see all fields in a single document. Use them for cross-field validation and document-wide checks.
Context Variables
| Variable | Type | Description |
|---|---|---|
fields | object | Map of field_id → { value, name, field_type, confidence } |
document_type | string | Document class name |
document_id | string | Document identifier |
field_name_to_id | object | Map of field display name → field ID |
scoped_field_ids | string[] | null | Field IDs this rule is scoped to (null = all fields) |
transaction_metadata | object | Read view of transaction.metadata |
document_metadata | object | Read view of this document's metadata |
get_field(field_name)
Get the value of a field by its display name.
const total = get_field('Total');
const subtotal = get_field('Subtotal');
set_field(field_name, value)
Set the value of a field by its display name. Used in normalization rules.
set_field('Full Name', `${get_field('First Name')} ${get_field('Last Name')}`);
get_scoped_fields()
Returns the fields this rule is scoped to (or all fields if unscoped). Returns an object of { field_id: { value, name, field_type, confidence } }.
const scoped = get_scoped_fields();
for (const [id, field] of Object.entries(scoped)) {
if (!field.value) {
set_error(`${field.name} is empty`);
}
}
highlight_fields(...field_names)
Nominate specific fields to be highlighted red in the viewer when the rule reports an error or warning. If not called, all fields in the rule's scope are highlighted.
highlight_fields('Invoice Number', 'Total Amount');
set_error('Invoice total does not match line items');
add_suggestion(field_name, value, label?, source?)
Suggest a value for a specific field (by display name).
const expected = get_field('Subtotal') + get_field('Tax');
add_suggestion('Total', expected, `Calculated: ${expected}`, 'Arithmetic Check');
Full Example: Validate Invoice Total
const lineItems = get_field('Line Items') || [];
const total = get_field('Total') || 0;
const sum = lineItems.reduce((acc, item) => acc + (item.amount || 0), 0);
if (Math.abs(sum - total) > 0.01) {
highlight_fields('Total');
set_error(`Line items sum (${sum}) doesn't match {field:Total} (${total})`);
} else {
set_success('Invoice total validated');
}
Transaction-Level API
Transaction-level rules see all documents in a transaction. Use them for cross-document validation.
Context Variables
| Variable | Type | Description |
|---|---|---|
documents | array | Array of document objects (see shape below) |
class_name_to_id | object | Map of document class name → class ID |
transaction_metadata | object | Read view of transaction.metadata |
Each element in the documents array has the following properties:
| Property | Type | Description |
|---|---|---|
document_id | string | Unique document identifier |
document_type | string | Document class display name (e.g. "Invoice") |
document_class_id | string | Stable document class ID (survives renames) |
fields | object | Map of field_id → { value, name, field_type, confidence } |
field_name_to_id | object | Map of field display name → field ID |
page_count | number | Number of pages in this document |
metadata | object | This document's metadata (also reachable via get_document_metadata(document_id)) |
You can iterate over documents directly or use the helper functions below.
get_document(type_name)
Find the first document matching a type name or class ID.
const invoice = get_document('Invoice');
const po = get_document('Purchase Order');
get_all_documents(type_name)
Find all documents matching a type name or class ID.
const receipts = get_all_documents('Receipt');
get_field(document_type, field_name)
Get a field value from a specific document type.
const invoiceTotal = get_field('Invoice', 'Total');
const poAmount = get_field('Purchase Order', 'Amount');
set_field(document_type, field_name, value)
Set a field value in a specific document type.
set_field('Summary', 'Total Amount', invoiceTotal + poAmount);
highlight_fields(...field_names)
Same as document-level: nominate fields to highlight on error/warning.
add_suggestion(field_name, value, label?, source?, document_id?)
Suggest a value for a field, optionally scoped to a specific document by ID.
add_suggestion('Total', 1000, '1000.00', 'Cross-doc Check', invoice.document_id);
Full Example: Validate Passport Matches Application
const application = get_document('Application');
const passport = get_document('Passport');
if (application && passport) {
const appNum = get_field('Application', 'Passport Number');
const passNum = get_field('Passport', 'Passport Number');
if (appNum !== passNum) {
highlight_fields('Passport Number');
set_error('Passport number mismatch between {document:Application} and {document:Passport}');
} else {
set_success('Passport numbers match');
}
} else {
set_warning('Missing required documents for validation');
}
Full Example: Validate Document Composition
This example validates that a transaction contains the expected document types, counts, and page counts. This is similar to what the Transaction Integrity Check preset generates, but written as custom code for full control.
// Count documents by type
const docCounts = {};
for (const doc of documents) {
const t = doc.document_type;
if (!docCounts[t]) docCounts[t] = 0;
docCounts[t]++;
}
const errors = [];
const warnings = [];
// Required: exactly 1 Invoice
if (!docCounts['Invoice']) {
errors.push('Missing required document: {document:Invoice}');
} else if (docCounts['Invoice'] > 1) {
errors.push('{document:Invoice}: expected 1, found ' + docCounts['Invoice']);
}
// Required: at least 1 Supporting Document, max 5
const supportCount = docCounts['Supporting Document'] || 0;
if (supportCount === 0) {
errors.push('Missing required document: {document:Supporting Document}');
} else if (supportCount > 5) {
errors.push('{document:Supporting Document}: max 5 allowed, found ' + supportCount);
}
// Optional: Contract (validate page count if present)
if (docCounts['Contract']) {
for (const doc of documents) {
if (doc.document_type === 'Contract' && doc.page_count < 2) {
warnings.push('{document:Contract}: expected at least 2 pages, has ' + doc.page_count);
}
}
}
// Warn on unexpected document types
const expected = new Set(['Invoice', 'Supporting Document', 'Contract']);
for (const t of Object.keys(docCounts)) {
if (!expected.has(t)) {
warnings.push('Unexpected document type: {document:' + t + '} (' + docCounts[t] + ' found)');
}
}
// Report
if (errors.length > 0) {
set_error(errors.join('\n'));
} else if (warnings.length > 0) {
set_warning(warnings.join('\n'));
} else {
set_success('Transaction composition validated');
}
The Transaction Integrity Check preset provides a visual builder for configuring document composition rules without writing code. It supports required/optional/conditional/not-allowed requirements, document count constraints, page count constraints, substitute document types (OR logic), and unknown document detection. The preset generates code using the same API shown above, so you can switch to custom code at any time to make adjustments.
Security
Rules execute inside a V8 isolate (via isolated-vm) with strict resource limits:
| Constraint | Limit |
|---|---|
| Execution timeout | 5 seconds per rule |
| Memory | 128 MB per isolate |
| Filesystem access | None |
| Network access | Only via fetch() with project-level domain allowlist |
| Node.js APIs | None (require, process, Buffer, etc. are unavailable) |
HTTP Allowlist
To enable fetch() in rules, configure the HTTP allowlist in your project settings:
- Go to Project Settings → Business Rules → HTTP Allowlist
- Enable the allowlist
- Add allowed domains (e.g.
api.example.com) - Set max requests per rule (default: 5) and request timeout (default: 5000ms)
Only http: and https: protocols are allowed. Subdomains of allowed domains are automatically permitted.
Tips
- Use
field_namefor display,field_idfor stability. Field names can be renamed by users; field IDs are immutable. The helper functionsget_field()andset_field()accept display names and resolve them to IDs internally. - Scope your rules. When creating document-level or transaction-level rules, set the field scope to limit which fields the rule applies to. This avoids running expensive rules on every field change.
- Return early on missing data. Always check for
null/undefinedvalues before operating on them, since extracted fields may be empty. - Use
highlight_fields()for focused feedback. Without it, all scoped fields are highlighted on error, which can be noisy. - Use
{field:...}and{document:...}tags in messages. These render as clickable badges in the Transaction Viewer, making it easy for users to jump to the relevant field or document. See Clickable Field & Document Tags. - Normalization rules run first. Field-level normalization rules execute during extraction, before validation rules. This means validation rules see already-normalized values.
- Carry reference data on
transaction.metadataordocument.metadata. Submit reference values (expected vendor, PO record, policy flags) alongside the document at upload time and rules can compare against them withget_transaction_metadata()/get_document_metadata(). The metadata-driven presets give you a no-code path for the common patterns; rules can alsoset_transaction_metadata(key, value)to persist enrichment back to the transaction for downstream steps.