Automation and AI
Validation Rules
Block invalid changes before they are saved, with a configurable error.
Validation rules are workflows that run before a change commits and can block it with a clear, configurable error. Unlike ordinary workflows (which run after something happens, in the background), a validation rule runs synchronously in the request path, so an invalid change is never persisted and no downstream workflow, webhook, message, or provider call runs for it.
How it works
A validation rule is an ordinary workflow whose trigger is set to Before (in the workflow builder, the trigger's "When should this run?" toggle). It uses the familiar building blocks:
- Trigger: Contact / Application / Conversation Created or Updated, set to Before.
- Conditions (
If): decide when the rule applies. Validation rules add condition sources for comparing state across the change:new: the proposed value (after the change)old: the value before the changechangedFields: which fields the request is changing (use thecontainsoperator)organization,user,payload: the acting org, user, and request body
- Lookups:
Get Contact by Id,Get Application by Id,Get Conversation,List Contacts,List Applications,List Conversations,List Reminders,List Media,Check Business Hours, andPersisted Storewith the Get operation read data outside the change. Their output is available to later conditions and messages as{{nodes.<id>.output.<path>}}. A rule may use at most 3 lookups. Lookups only read:Persisted Store's writing operations and the reminder create/edit/delete actions are not available in a validation rule. To act on "already seen", set the key from a regular (after) workflow and read it here with Get, using the same Namespace (required here) and the same key value. The two sides spell it differently:{{contact._id}}in the after-workflow,{{new._id}}in the rule. - Stop: end the rule and allow the change (the default for non-matching branches).
- Reject Change: block the change with a configurable
messageand optionalfield.
Example: Contact Updated (Before) → If
changedFieldscontainsstatusandold.status == Approvedandnew.status == Signed→ Ifnew.signedAtis empty → Reject: "Cannot move to signed status without a signature timestamp".
Supported resources
| Resource | Events | Trigger to use |
|---|---|---|
| Contact | create, update, delete | Contact Created / Contact Updated (Before) |
| Application | create, update, delete | Application Created / Application Updated (Before) |
| Conversation | update, resolve, delete | Conversation Updated (Before) |
| Outbound message | send (SMS + email) | Message Trigger (Before) |
| Outbound call | start | Call Starting (Before) |
Status/stage transitions are plain updates, so author a rule on the Updated
trigger with old.status / new.status conditions (see the example above).
Delete rules reuse the entity's Updated (Before) trigger, so add a condition
event == 'delete' to gate deletions (the event condition source also exposes
create / update / send / start / resolve). Send rules block an outbound SMS or
email before it leaves the provider; call rules block an outbound call before
it is placed.
Activation
A validation rule is only enforced once enabled. Enabling is blocked (HTTP 422)
if the rule uses unsupported (asynchronous) nodes, has no reachable Reject Change
action, has a Reject with an empty message, or references an unknown condition source.
Error response
When a blocking rule rejects a change, the API responds with 422 Unprocessable Entity
and a structured body. The same message is shown in the UI.
{
"success": false,
"error": {
"code": "VALIDATION_RULE_FAILED",
"message": "Cannot move to signed status without a signature timestamp",
"field": "status",
"ruleId": "665f0c2e1a2b3c4d5e6f7890"
}
}field and ruleId are included when available. Warning-severity rules are
recorded for audit but do not block the change.
Audit
Every evaluation is recorded as a validation run (rule id, target action, pass/block/warn result, and the message shown). Editing or disabling a rule never changes historical audit records: runs are append-only and there is no API to modify or delete them.
Read the audit trail with GET /api/workflows/validation-runs (org-scoped,
paginated):
GET /api/workflows/validation-runs?result=block&days=7&page=1&limit=20| Query param | Description |
|---|---|
page, limit | Pagination (limit caps at 100, default 20) |
ruleId | Filter to one rule (the workflow's id) |
result | pass, block, or warn |
resourceType | Contact, Application, Conversation, message, call |
event | create, update, delete, send, resolve, start |
resourceId | Filter to one record |
days | Only runs from the last N days |
{
"success": true,
"data": [
{
"ruleId": "665f0c2e1a2b3c4d5e6f7890",
"ruleName": "Require signature before Signed",
"resourceType": "Contact",
"event": "update",
"result": "block",
"message": "Cannot move to signed status without a signature timestamp",
"field": "status",
"changedFields": ["status"],
"createdAt": "2026-06-11T10:15:00.000Z"
}
],
"pagination": { "page": 1, "limit": 20, "total": 1, "totalPages": 1 }
}Scope and caveats
- Changes made by workflows are validated too. When an (after) workflow's
action creates or updates a contact, application, or conversation, before-rules
still apply. A blocking rule fails that workflow action, and the failure is
visible in the workflow's run history. System-initiated changes have no acting
user or request body, so conditions on the
user/payloadsources evaluate against empty values for those runs. - Bulk administrative operations bypass validation. Internal maintenance paths that rewrite many records at once (pipeline stage migrations, org transfers, contact separation) do not run per-record validation rules.
- Rule changes propagate within ~30 seconds. Active rules are cached per-server with a 30-second TTL; after enabling, disabling, or editing a rule, allow up to 30 seconds for every server to pick up the change.