Skip to main content

Business Rules

Under Review

This article is currently under review. Some content may be incomplete or inaccurate.

Business rules let you write custom JavaScript logic that validates and normalizes extracted field values. Rules run inside a secure sandbox: you have access to field data and a set of helper functions, but no filesystem, network (unless explicitly allowed), or Node.js APIs.

Rule Levels

Each rule operates at one of three levels:

LevelScopeTypical use
FieldSingle field valueFormat validation, value normalization
DocumentAll fields in one documentCross-field checks (e.g. line items sum = total)
TransactionAll documents in a transactionCross-document checks (e.g. passport matches application)

Rule Types

TypeWhen it runsPurpose
NormalizationAutomatically during the Extract step (and on field edits in the Transaction Viewer)Rewrite values to a canonical form (dates, currencies, casing, etc.)
ValidationAt the dedicated Validate step (and on field edits in the Transaction Viewer)Check values and emit errors/warnings displayed in the Transaction Viewer

Presets vs Custom Code

You can create rules in two ways:

  • Presets: choose from a library of configurable templates (allowed values, regex, date range, number range, arithmetic check, transaction integrity check, metadata match, etc.) and fill in parameters. The code is generated for you. A handful of presets (arithmetic, transaction integrity, compare fields, validate fields against metadata) use a structured visual builder rather than a flat form.
  • Custom code: write JavaScript directly in the Monaco editor with IntelliSense support.

Preset-generated code uses the same public scripting API described below. You can always switch a preset rule to custom code by copying the generated code and editing it directly.


Rule Properties

Each rule has metadata properties that control how it behaves. These are configured in the rule creation/edit dialog and are handled by the engine externally: they are not embedded in the rule's JavaScript code.

PropertyDefaultDescription
EnabledOnWhen disabled, the rule is skipped entirely (it never executes).
SeverityErrorControls how validation failures are reported. When set to Warning, all set_error() calls in the rule are downgraded to warnings after execution.
Confirm fields on successOnWhen enabled, all fields engaged in a validation rule are auto-confirmed if the rule passes without errors.
WeightNoneDisplay ordering only: higher-weight results appear first in the validation panel. Does not affect execution order.

Severity override behavior

The severity property acts as a one-directional, post-execution override:

  • Error (default): set_error() produces errors, set_warning() produces warnings, with no override applied.
  • Warning: all set_error() calls are downgraded to warnings. set_warning() calls remain warnings. The override never upgrades warnings to errors.
Severity override and mixed error/warning rules

If your rule intentionally uses both set_error() and set_warning() for different conditions, setting severity to Warning will collapse that distinction: both become warnings. If you need fine-grained control (some conditions as errors, others as warnings), leave severity at Error and call set_error() or set_warning() directly in your code as needed.


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');
Field-level messages and field names

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.

OptionTypeEffect
trimbooleanRemove leading/trailing whitespace
lowercasebooleanConvert to lowercase
uppercasebooleanConvert to uppercase
removeSpacesbooleanCollapse all whitespace runs to a single space
removeSpecialCharsbooleanStrip 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
FunctionAvailable atDescription
get_transaction_metadata(key?)field, document, transactionRead transaction.metadata. No arg returns the whole object.
set_transaction_metadata(key, value)field, document, transactionWrite a key into transaction.metadata. Persisted after the run.
get_document_metadata(key?)field, documentRead the parent document's metadata.
set_document_metadata(key, value)field, documentWrite a key into the parent document's metadata.
get_document_metadata(doc_id_or_index, key?)transactionRead a specific document's metadata.
set_document_metadata(doc_id_or_index, key, value)transactionWrite a key into a specific document's metadata.

The metadata-driven presets (see below) 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:

TagAppearanceClick action
{field:Invoice Number}Blue badgeSelects the field in the Data Panel
{document:Passport}Purple badgeNavigates 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

VariableTypeDescription
valueanyCurrent field value
field_namestringDisplay name of the field
field_idstringStable field identifier (survives renames)
field_typestringData type (string, number, date, etc.)
field_settingsobjectField-specific settings from project configuration
document_idstringParent document ID
document_class_idstringParent document class ID
transaction_metadataobjectRead view of transaction.metadata (also via get_transaction_metadata())
document_metadataobjectRead 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

VariableTypeDescription
fieldsobjectMap of field_id{ value, name, field_type, confidence }
document_typestringDocument class name
document_idstringDocument identifier
field_name_to_idobjectMap of field display name → field ID
scoped_field_idsstring[] | nullField IDs this rule is scoped to (null = all fields)
transaction_metadataobjectRead view of transaction.metadata
document_metadataobjectRead 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

VariableTypeDescription
documentsarrayArray of document objects (see shape below)
class_name_to_idobjectMap of document class name → class ID
transaction_metadataobjectRead view of transaction.metadata

Each element in the documents array has the following properties:

PropertyTypeDescription
document_idstringUnique document identifier
document_typestringDocument class display name (e.g. "Invoice")
document_class_idstringStable document class ID (survives renames)
fieldsobjectMap of field_id{ value, name, field_type, confidence }
field_name_to_idobjectMap of field display name → field ID
page_countnumberNumber of pages in this document
metadataobjectThis 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');
}
Transaction Integrity Check Preset

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.


Metadata-Driven Presets

Four presets are dedicated to comparing extracted values against (or populating them from) reference data carried on transaction.metadata or document.metadata. They are the no-code path to the metadata helpers documented above.

Populate Field From Metadata (field, normalization)

Reads a key from transaction.metadata or document.metadata and writes it into the bound field. Useful when the field is sometimes blank and the correct value lives on the transaction (e.g. supplier ID provided at intake).

SettingDescription
Metadata Sourcetransaction or document
Metadata KeyKey to read from the chosen metadata bag
Only If EmptyWhen enabled, only populates when the field is currently empty (does not overwrite extracted values)

On a successful write the rule sets a success message explaining where the value came from and the field is auto-confirmed (treated as 100% confidence).

Validate Field Against Metadata (field, validation)

Compares the field value against a metadata key. Exact mode is a trimmed case-insensitive compare; fuzzy mode uses Jaro-Winkler similarity, and a close fuzzy match also normalizes the field value to the canonical metadata value.

SettingDescription
Metadata Sourcetransaction or document
Metadata KeyKey to compare against
Comparison Modeexact or fuzzy
Fuzzy ThresholdSimilarity 0.0-1.0 (fuzzy mode only)
Allow Empty FieldSkip the comparison when the field is empty
Allow Empty MetadataSkip the comparison when the metadata key is missing or empty

On mismatch the rule emits an error and suggests the metadata value so the reviewer can apply it with one click.

Populate Fields From Metadata (document, normalization)

The document-level counterpart of the field-level populate preset. One rule populates many fields in one go.

SettingDescription
Metadata Sourcetransaction or document
MappingsOne Field Name = metadata_key per line
Only If EmptyOnly populate fields that are currently empty

Use a run condition on the rule (the standard rule-level condition editor) to gate when the population fires.

Validate Fields Against Metadata (document, validation)

The most flexible of the four: uses a structured per-pair builder like the Catalog Lookup preset. Each row maps one document field to one metadata key, with its own comparison mode and allow-empty flags.

For each pair, pick one of three modes:

ModeBehavior
ExactTrimmed case-insensitive equality. Failure → rule errors.
FuzzyJaro-Winkler similarity above the global threshold. Failure → rule errors.
Copy from MetadataThe field value is written from the metadata key, but only after every matcher (exact / fuzzy) pair has passed.

For the rule to pass, all matcher pairs must match (or be skipped via their allow-empty flags). Once all matchers pass, every copy_from_metadata pair writes its metadata value into its field.

Typical example: verify and enrich a supplier extracted from an invoice against a supplier record carried on transaction metadata:

FieldMetadata KeyMode
Vendor Namesupplier_nameFuzzy
Vendor Zipsupplier_zipExact
Tax IDsupplier_taxCopy from Metadata

If the name fuzzy-matches and the zip is exact, the tax ID is copied from transaction.metadata['supplier_tax'] into the document field. If either matcher fails, the rule errors, highlights the mismatching field, and offers the metadata value as a suggestion, without touching the tax ID.

Switching to custom code

All four presets generate plain JavaScript that uses the public scripting API (get_transaction_metadata, set_field, add_suggestion, etc.). You can convert any preset rule to custom code at any time if you need behavior the builder doesn't expose.


Security

Rules execute inside a V8 isolate (via isolated-vm) with strict resource limits:

ConstraintLimit
Execution timeout5 seconds per rule
Memory128 MB per isolate
Filesystem accessNone
Network accessOnly via fetch() with project-level domain allowlist
Node.js APIsNone (require, process, Buffer, etc. are unavailable)

HTTP Allowlist

To enable fetch() in rules, configure the HTTP allowlist in your project settings:

  1. Go to Project Settings → Business Rules → HTTP Allowlist
  2. Enable the allowlist
  3. Add allowed domains (e.g. api.example.com)
  4. 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_name for display, field_id for stability. Field names can be renamed by users; field IDs are immutable. The helper functions get_field() and set_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/undefined values 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.metadata or document.metadata. Submit reference values (expected vendor, PO record, policy flags) alongside the document at upload time and rules can compare against them with get_transaction_metadata() / get_document_metadata(). The metadata-driven presets give you a no-code path for the common patterns; rules can also set_transaction_metadata(key, value) to persist enrichment back to the transaction for downstream steps.