Validation Rules

A validation rule is one PHPScript snippet per table that runs before every create, update, and delete. Return a non-empty string to block the write — the string is shown to the person making the change — or return nothing to let it through.

It's the place for rules that a Required checkbox can't express: cross-field checks, status locks, line-item totals, and "who is allowed to change this, and how".

Adding a rule

In a table's Configure screen, open the General tab and write the rule in the Validation editor. It's checked for syntax when you save — a broken script is rejected on save, not stored. Clear the editor to remove the rule.

When it runs

The rule runs at the moment of the write, on every path — the record form, web forms, the public API, automations, and imports all funnel through it. For a record with line items, the parent and all its lines are validated together as one atomic unit: if the rule blocks, nothing persists — no parent, no orphan lines.

Two things to know:

  • It fails closed. If the rule itself errors or exceeds its 5-second cap, the write is blocked, not silently allowed — a broken integrity gate is worse than an annoying one. Keep it fast and simple.
  • It only reads. Use the query helpers (records_query(), record_get(), etc.) to look things up. Don't write from a validation rule.

What the rule can use

Variable What it holds
$record The proposed values (after the edit), keyed by field id. null on delete.
$before The current stored values — on update and delete only (absent on create). Compare $record against $before to see what changed.
$lines The proposed line items, keyed by sub-table id (for records with sub-tables).
$user The acting user (id, email, name), or null when there's no signed-in user (e.g. a public web-form submission).
$via How the write arrived (see below).

$via — the write origin

$via is a string naming where the write came from, so a rule can allow or block a change based on how it was made:

$via Origin
"ui" A signed-in user editing on the record form
"api" The public API (any API key)
"flow" An automation
"webform" A public web form submission
"email" Email-to-record create
"system" An internal/background write with no other origin

Returning a result

  • Return an empty string, null, or nothing → the write passes.
  • Return a non-empty string → the write is blocked and that string is shown to the user.

On the record form a blocked write shows the message in a dialog and leaves the record unchanged. Through the public API the write is rejected with an error carrying your message (see the API errors reference).

Examples

Require a field before a status

// A deal can't be marked "won" without a close date.
if ( $record["stage"] == "won" && empty($record["close_date"]) )
    return "Set a close date before marking the deal won.";

Lock a record once it reaches a status

Compare $before (what's stored) to freeze a record after it's been posted:

// Once posted, an invoice can't be edited or deleted.
if ( $before["status"] == "posted" )
    return "This invoice is posted and can no longer be changed.";

Because $record is null on delete but $before is still set, the same rule blocks both edits and deletes of a posted record.

Make a table editable only via API or flows

Use $via to keep a table as a system of record — your integrations and automations maintain it, and people can't change it by hand:

// Only the API and automations may write here; block interactive edits.
if ( ! in_array($via, ["api", "flow"]) )
    return "This table is maintained by our integrations and can't be edited directly.";

Swap the allow-list to fit the case: ["api"] for API-only, or add "webform" to also accept public form submissions.

Restrict a single field instead of the whole write

To make one field uneditable by regular users — rather than gating the whole record — you usually don't need a rule at all. Mark the field Admin Only in its field settings: non-admins see it locked, while admins (and an admin-scoped API key) can still write it. Reach for a validation rule when the lock depends on data (a status, another field, the write origin) rather than just on the user's role.

Notes and limits

  • A rule set on a sub-table doesn't fire on its own — line items are validated by the parent table's rule as part of the whole-unit write. Put line-item checks in the parent's rule and read $lines.
  • A parent-only update (no line items sent) leaves $lines empty. Send the lines if a rule needs to check them.
  • The rule runs read-only and returns a single message. Field-by-field error mapping isn't available yet.