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:

  1. Static validation - the form's own required/min/max/regex rules (see the Form Editor). Stops here if anything fails.
  2. validate.js - cross-field validation (below). Stops here if it writes any errors.
  3. process.js - layout assembly (below): decides which layout partials to include, and can prepare presentation data for blocks to use.
  4. 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.";
}

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
}

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:

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:

  1. 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.js and a block's own expression run in separate scripting contexts; the only bridge between them is a new property assigned onto inputs (see Handing computed data to blocks above).
  2. Is the property name actually new, not the same name as an existing submitted field? Reassigning inputs.options = ... (a field that already existed before process.js ran) 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.