DocsSchema & validation
Schema and validation
Model every field, answer, condition, setting, and safe schema change against the shared contract.
Start from the schema shape
A form schema is the shared contract used by the builder, SDK renderers, public API, response validation, exports, and delivery. JSX and defineForm() produce the same schema. The code form id is the workspace-stable sync handle; Fillo returns a separate canonical formId for published links and submissions.
Every schema has version: 1, a title, at least one page, an ordered block list, and settings. Fields create answers. Content blocks shape the reading flow without adding answer keys.
import { defineForm } from "@usefillo/core";
export const supportIntake = defineForm({
id: "support-intake",
title: "Tell us what happened",
pages: [
{
id: "request",
blocks: [
{
id: "topic",
kind: "select",
label: "What do you need help with?",
required: true,
options: [
{ id: "billing", label: "Billing" },
{ id: "technical", label: "Technical issue" },
],
},
{
id: "details",
kind: "long_text",
label: "Tell us more about {{topic}}",
required: true,
visibleIf: [{ fieldId: "topic", op: "answered" }],
},
{
id: "account_id",
kind: "hidden",
paramName: "account",
},
],
},
],
settings: { showProgress: true, saveProgress: true },
});Treat every id as stored data
- Use a permanent code form handle. Reusing it updates the same Fillo form; changing it creates a different sync identity.
- Page ids must be unique within the form. Block ids must be unique across every page because field ids become keys in
ResponseData. - Choice option ids, ranking option ids, and matrix row and column ids are stored answer values. Labels may change without rewriting those values.
- Ids are strings from 1 to 128 characters. Never derive them from array position, visible copy, translated labels, or a random value generated during render.
- A schema supports 1 to 50 pages, up to 500 blocks per page, and up to 200 options, rows, or columns in one list.
Renaming an id is a data change
Changing company_size to team_size does not rename historical answers. It creates a new answer key. Keep the id and change the label, or add a new field and update downstream consumers deliberately.
Field kinds and answer shapes
Every field accepts id, label, optional description, required, placeholder, and visibleIf where those properties apply. The table adds each kind's stored answer shape and field-specific rules.
| Kind | JSX | Stored answer | Validation and configuration |
|---|---|---|---|
short_text | Fillo.Text | string | Optional maxLength; otherwise a 2,000 character safety cap. |
long_text | Fillo.LongText | string | Optional maxLength; otherwise a 20,000 character safety cap. |
email | Fillo.Email | string | Valid email syntax and optional maxLength. |
url | Fillo.Url | string | An absolute http: or https: URL, with optional maxLength. |
phone | Fillo.Phone | string stored in E.164 form | Server validation is authoritative; defaultCountry is a two-letter country code. |
number | Fillo.Number | finite number | Optional min and max; numeric strings are normalized before validation. |
select | Fillo.Select | option id or Other string | One known option id, or one free-text value up to 500 characters when allowOther is on. |
multi_select | Fillo.MultiSelect | string[] | Unique known option ids, plus at most one Other value when enabled. |
dropdown | Fillo.Dropdown | option id or Other string | One known option id, or one free-text value up to 500 characters when allowOther is on. |
checkbox | Fillo.Checkbox | boolean | Required means the value must be true; appearance may be checkbox or toggle. |
rating | Fillo.Rating | integer number | From 1 through max, which defaults to 5. CSAT requires a 1 through 5 scale. |
linear_scale | Fillo.Scale | integer number | Between min and max, defaulting to 1 and 10. NPS requires 0 through 10. |
ranking | Fillo.Ranking | ordered string[] | Every option id must appear exactly once. |
matrix | Fillo.Matrix | Record<rowId, columnId> | Only declared row and column ids; a required matrix needs an answer for every row. |
signature | Fillo.Signature | data:image/... string | Must be an image data URL produced by the signature control. |
date | Fillo.Date | YYYY-MM-DD string | Must be a real calendar date, not only a string with the right shape. |
file_upload | Fillo.FileUpload | FileValue[] | Completed file references only; maxFiles, per-file size, and accept are enforced. |
hidden | Fillo.Hidden | string | Filled from paramName or defaultValue, never rendered, and never required. |
custom | Fillo.Custom | arbitrary JSON | Core checks required presence only; the custom component owns richer validation. |
Content blocks
Content blocks belong in the same ordered blocks array as fields. They need stable block ids for durable schema structure and may carry visibleIf, but they never create a response value.
| Kind | JSX | Schema content | Behavior |
|---|---|---|---|
heading | Fillo.Heading | text: string | Displays a heading and supports answer piping. |
paragraph | Fillo.Paragraph | text: string | Displays supporting copy and supports answer piping. |
divider | Fillo.Divider | No content property | Adds a visual break and never creates an answer. |
Read and write response data by field id
ResponseDatais an object keyed by field id. Optional unanswered fields are absent; do not depend on every schema field having a key.- Choice fields store option ids. Use Fillo's formatting helpers or the response
formattedpayload when a destination needs labels. - Numbers, ratings, and scales are stored as numbers after normalization. Matrix answers map row ids to column ids. Files are arrays of completed file references.
- A custom field may store any JSON value. Its renderer and your downstream code must agree on the shape.
- Each response is anchored to the schema version it answered. The response grid and CSV retain removed field ids, while live integrations work from the current schema.
Validation runs at schema and response boundaries
- Fillo normalizes and validates untrusted schemas before storing or rendering them. Invalid top-level shapes, unknown kinds, duplicate ids, invalid number ranges, and unsupported schema versions fail. Malformed field blocks such as a choice with no valid options are removed, and a schema with no valid block is rejected.
- The client gives immediate field feedback, but the submission API validates the response again. Treat the server result as authoritative.
- Only fields visible for the submitted answers are validated and retained. A stale answer from a field hidden by logic is removed from the accepted data.
- Strings are trimmed except custom-field strings. Numeric strings are normalized for number, rating, and scale fields. Phone answers receive authoritative server validation and E.164 normalization.
- File answers are accepted only after the upload protocol has created and verified the referenced file for the same form.
Model choices with stable option ids
- Store machine-friendly option ids such as
technical; show respondents changeable labels such asTechnical issue. allowOtherstores the respondent's text directly when it is not a declared option id. Downstream code must handle both declared ids and free text.shuffleOptionschanges presentation order only. It does not change the option list or stored values.- A ranking answer contains every option id in respondent order. Adding or removing ranking options changes the required answer set.
- Matrix rows and columns each have their own stable ids. A required matrix must contain one valid column id for every row.
Use visibleIf for conditional structure
Attach visibleIf to a field or content block. Every condition in the array must match, so multiple conditions use AND behavior. Reference the controller by field id and compare choice answers to option ids, not labels.
| Operator | Behavior |
|---|---|
eq | Exact value match; a multi-select matches when it contains the value. |
neq | The field must be answered and different from the value. |
contains | Case-insensitive substring for text, or membership for an array. |
gt / lt | Finite numeric comparison, including numeric input strings. |
answered | True when the effective value is not empty. |
not_answered | True when the effective value is empty. |
Hidden controllers count as unanswered
If logic hides a controlling field, its stale value cannot keep a dependent field visible. This resolves chained conditions safely and matches submission validation.
Pipe answers into respondent-facing copy
Use {{field_id}} in a field label or description, or in heading and paragraph text. Fillo substitutes the current answer for display only. Choice ids become option labels, arrays join with commas, and an unanswered field becomes an empty string.
Piping does not change stored answers and is not a templating or authorization system. Never put a secret in a schema and expect a conditional block or piping token to hide it from someone who can load the published schema.
Know who owns each setting
All settings are part of FormSchema, but a browser-visible pk_ sync cannot control settings with mail or pre-submission privacy effects. On public-key sync, Fillo strips those values from a new form and preserves the dashboard value on an existing form.
| Setting | Effect | Public-key sync |
|---|---|---|
submitMode | Use a normal submit button or eligible one-answer auto-submit behavior. | Incoming schema |
submitLabel | Override the default submit button label. | Incoming schema |
successTitle | Set the success screen heading. | Incoming schema |
successMessage | Set the success screen supporting copy. | Incoming schema |
redirectUrl | Send the respondent to an absolute HTTP or HTTPS URL after success. | Incoming schema |
showProgress | Show progress through a multi-page form. | Incoming schema |
responseLimit | Limit repeats by browser, identifying field, or identify context. | Incoming schema |
notifyEmail | Email a workspace-selected address after a response. | Dashboard preserved |
sendReceipt | Send a receipt to the first answered email field. | Dashboard preserved |
saveProgress | Persist in-progress answers so a respondent can resume. | Incoming schema |
draftAnswersVisible | Let workspace members view saved, unsubmitted answer content. | Dashboard preserved |
resumeEmails | Send one resume link to an eligible respondent after abandonment. | Dashboard preserved |
resumeUrl | Choose the HTTP or HTTPS page that receives an embedded resume link. | Dashboard preserved |
draftDigest | Send daily drop-off counts and verified respondent context, never answer content. | Dashboard preserved |
Private pushes have broader authority
A reviewed CLI push uses a private management token and replaces the complete schema, including dashboard-preserved keys. For a code-managed form, saveProgress and responseLimit stay code-controlled; dashboard forms edit them in Settings.
Make version-safe schema changes
Prefer additive optional fields
An optional field does not block an older client or a respondent who never sees it. A new visible required field changes what a valid submission needs.
Change copy without changing ids
Labels, descriptions, placeholders, and option labels may evolve while stable ids preserve raw answers and downstream keys.
Use a new id for a new answer shape
Changing a field from
selecttomulti_select, or reusing an old option id for a different meaning, makes one key represent incompatible data. Add a new field instead.Review logic and piping references
When removing a field or option, search
visibleIf, piping tokens, response limits, prefill, webhooks, exports, and destination mappings for its id.Stage and test the published path
Public-key sync stages changes on claimed live forms. Test conditional branches, validation, uploads, and a real response before publishing. A private CLI push publishes directly unless you use
--draft.