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 change
    • changedFields: which fields the request is changing (use the contains operator)
    • 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, and Persisted Store with 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 message and optional field.

Example: Contact Updated (Before) → If changedFields contains status and old.status == Approved and new.status == Signed → If new.signedAt is empty → Reject: "Cannot move to signed status without a signature timestamp".

Supported resources

ResourceEventsTrigger to use
Contactcreate, update, deleteContact Created / Contact Updated (Before)
Applicationcreate, update, deleteApplication Created / Application Updated (Before)
Conversationupdate, resolve, deleteConversation Updated (Before)
Outbound messagesend (SMS + email)Message Trigger (Before)
Outbound callstartCall 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 paramDescription
page, limitPagination (limit caps at 100, default 20)
ruleIdFilter to one rule (the workflow's id)
resultpass, block, or warn
resourceTypeContact, Application, Conversation, message, call
eventcreate, update, delete, send, resolve, start
resourceIdFilter to one record
daysOnly 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 / payload sources 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.

On this page