Two optional scripts, validate.js and process.js, cover anything a form's
declarative validation rules or a block's inline template can't. Both are real
JavaScript, edited via the Script Editor (or
any external editor). Neither is required - a project with no cross-field checks and
no conditional layout content doesn't need either file.
The processing pipeline
Hitting Generate on Fill Out a Form runs, in this exact order:
- Static validation - the form's own
required/min/max/regexrules (see the Form Editor). Stops here if anything fails. validate.js- cross-field validation (below). Stops here if it writes anyerrors.process.js- layout assembly (below): decides which layout partials to include, and can prepare presentation data for blocks to use.- Render - the assembled layout is drawn onto the page canvas and saved to the
project's
output/folder.
Nothing about a submission is saved anywhere except the generated PDF itself - see Document Project Structure.
validate.js - cross-field validation
Runs after the form's own required/min/max/regex rules pass, and before anything gets
generated. Two things are available: inputs (everything submitted, keyed by field
ID) and errors (a plain object you write messages into):
if (inputs.sale_price <= 0) {
errors["sale_price"] = "Sale price must be greater than zero.";
}
if (inputs.year < 1900 || inputs.year > new Date().getFullYear() + 1) {
errors["year"] = "Enter a valid model year.";
}
- Assign to
errors["field_id"]for a message that shows inline under that field. There is no general/form-level channel (errors.push(...)doesn't exist and will throw) - attach every message to the field it's most relevant to. - Leave
errorsuntouched when there's nothing to report - its final state after the script runs is all that matters, not the return value. - This is the right place for anything that compares two or more fields against each other - a computed field's simple expression syntax (see Conditions & Expressions below) can't do that.
process.js - layout assembly
Decides which layout partials actually get included when a PDF is
generated. Two things are available: inputs (same submitted-data map as
validate.js) and process(filename), a function that queues up a layout partial to
be merged in:
process("overlay.json"); // always include the base layout
if (inputs["is_certified"]) {
process("certified_badge.json"); // only when the checkbox is on
}
- If
process.jsdoesn't exist at all, the app behaves as if it contained exactlyprocess("overlay.json");- so a project with nothing conditional doesn't need this file. - A partial named that doesn't exist is silently skipped, not an error - don't rely on this to signal a problem.
- Every block
idacross everything actually included in one run must be unique.
Handing computed data to blocks
You can also compute presentation data here and hand it forward to a block, instead of
writing a long formula inline in the block's own JSON - assign it as a new property
on inputs:
inputs.gridData = [];
for (var i = 0; i < inputs.options.length; i++) {
var option = inputs.options[i];
inputs.gridData.push(
{ text: (i + 1) + '. ' + option.description },
{ text: '$' + option.price, 'text-align': 'right' }
);
}
process("overlay.json");
(This particular example - turning a repeating group into grid cells - is common enough to have its own walkthrough: Generating Grid Data.)
A grid block (or any block) can then just reference inputs.gridData directly, keeping
the block's own JSON free of formatting logic:
{ "id": "options_grid", "type": "grid", "columns": 2, "data": "inputs.gridData" }
Two things worth understanding about how this actually works:
- Only new property names get forwarded.
gridDataabove didn't exist beforeprocess.jsran. Reassigning an existing submitted field this way (e.g.inputs.msrp = '$' + inputs.msrp) has no effect on what a block sees - it's silently dropped before rendering. This is deliberate: it means aprocess.jsedit can never change how some unrelated block elsewhere in the project renders an original field (dates in particular - the app applies its own date formatting to a raw submitted date, which a reassignment would bypass). Always compute into a fresh name, never overwrite an original one. process.jsand a block's own expression run in genuinely separate scripting contexts - there's no shared memory between the "decide what to include" step and the "render each block" step other than throughinputs.*properties, the same wayprocess.jsandvalidate.jsdon't share state either. If you set a plain local variable inprocess.js(var x = 5;) rather than a property oninputs, no block will ever see it.
Conditions & Expressions
A block's condition property and a form's computed-field expression (see Form
Editor) both use a different,
simpler engine than validate.js/process.js/block templates - arithmetic and
field access only, not full JavaScript:
is_certified == true
sale_price > 0
trim_level == 'Sport' || trim_level == 'Premium'
inputs['sale_price'] * 1.08
Bare field names and inputs['field_id'] bracket access both work and mean the same
thing. Falsy (false, 0, "", null) hides the block or fails the condition;
leaving condition off entirely always shows the block.
There's no js:-prefixed escape hatch for a condition - if you need something more
complex than this syntax allows, that belongs in validate.js (rejecting the
submission) or a {...} block template segment (formatting for display), not in
condition.
Where each engine applies
It's easy to mix these up since they all look like "put a formula here" - here's the actual split:
| Where | Engine | What it can do |
|---|---|---|
| validate.js, process.js | Real JavaScript (flutter_js) | Anything - loops, method calls, string building |
| A block's text/data inside {...} (or a grid's data, unwrapped) | Real JavaScript (flutter_js) | Same as above, evaluated fresh per block at render time |
| A block's condition | The expressions package | Arithmetic, comparisons, field access only - no method calls |
| A computed field's expression in form.json | The expressions package | Same restricted syntax as condition |
Debugging
The most common "it just doesn't work" report is a computed value in process.js
doesn't show up in the PDF, with no error anywhere. Check two things, in order:
- Did you assign it onto
inputs, not a plain local variable?// Wrong - nothing outside this script will ever see `myData`. var myData = inputs.options.map(...); // Right - a NEW property on inputs, visible to any block afterward. inputs.myData = inputs.options.map(...);process.jsand a block's own expression run in separate scripting contexts; the only bridge between them is a new property assigned ontoinputs(see Handing computed data to blocks above). - Is the property name actually new, not the same name as an existing submitted
field? Reassigning
inputs.options = ...(a field that already existed beforeprocess.jsran) is silently ignored - compute into a different name instead (inputs.optionsFormatted,inputs.gridData, whatever reads clearly).
A block renders nothing, with no error message. Usually by design, not a bug -
several block types quietly omit themselves rather than showing a broken placeholder:
an image/signature block with a missing/unset source, a QR block whose data evaluates
to an empty string, a grid block whose data expression evaluates to an empty array (or
errors - a broken JS expression inside a grid's data fails silently, same as
everywhere else block templates run). Double-check the field/expression actually has a
value for the data you're testing with.
For everything else (JSON syntax errors, autocomplete not suggesting, layout looking different in the editor vs. the printed PDF), see Troubleshooting.