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
$linesempty. 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.