Plain JavaScript. Optional - only needed for checks that compare multiple fields against each other; form.json's own required/min/max/regex rules already cover single-field checks. Edited via the Script Editor. Runs after those static rules pass and before process.js.

Name Type Meaning
inputs object The submitted values, keyed by field id. Mutable - see below.
errors object Write validation messages into it. Final state when the script finishes is what matters.
if (inputs.sale_price <= 0) {
  errors["sale_price"] = "Sale price must be greater than zero.";
}

errors["field_id"] shows inline under that field; errors[""] (the empty string key) is the general/form-level error channel, shown as a banner. Writing anything to errors stops the submission there - process.js doesn't run.

Computing a value that also needs checking

Assign a new (or reassign an existing) property onto inputs - the same read/write contract process.js has. As long as the script reports no errors, whatever's on inputs when it finishes carries forward exactly like a directly-submitted field, available to process.js and every block:

inputs.price_with_tax = inputs.sale_price * 1.08;
if (inputs.price_with_tax > 100000) {
  errors["sale_price"] = "Price with tax can't exceed $100,000.";
}

This is the only way to compute a derived value in form.json - there's no separate "computed field" concept. A value computed this way is calculated once, at submission time, and baked into that PDF; editing the script later doesn't retroactively change a PDF already generated.

Debugging

console.log()/console.warn()/console.error() all work here too - see JavaScript API's Console section for where their output goes.