Field types
A Field has a key, a label, a type and required. Scale, satisfaction and choice carry a little more; a rule can show a Field only when an earlier one matches.
Reference
Every type also takes default, hidden and locked — see below
— and when, a rule that shows the Field only when an earlier one matches — see
Conditional Fields.
| type | in the Studio | value in JSON | example | extra keys |
|---|---|---|---|---|
text | text | string | "Ana" | |
textarea | long answer | string | "Loved the product" | |
date | date | YYYY-MM-DD string | "2026-10-01" | minDate, maxDate, afterField |
dropdown | dropdown | string, one of the options or an allowed Other answer | "University of Bristol" | options, searchable, allowOther |
content | text block | no answer | — | description |
email | string with @ | "ana@acme.com" | ||
url | url | http or https address | "https://acme.com/pricing" | |
number | number | finite number | 5 | |
scale | scale | integer between min and max | 9 | min, max |
satisfaction | satisfaction | integer 1 to 5 | 5 | minLabel, maxLabel |
select | choice | string, one of the options | "pro" | options |
multiselect | multiple choice | string[], within options | ["api","webhooks"] | options |
emoji | emoji | string, one of the options | "😍" | options, each one emoji |
Rules
The key starts with a letter and uses only a-z, 0-9 and _, at most 40
characters. A Form takes up to 40 Fields, and a choice up to 500 options.
On a scale, min must be less than max, both integers between −100 and 100.
satisfaction is always 1 to 5; minLabel and maxLabel are the text under the
faces at each end. On a select, multiselect or emoji, options can be neither
empty nor contain repeats; on an emoji, each option is a single emoji — a face, a
flag, a hand with a skin tone — and the public page draws them as a row of large
buttons. A select option may contain an emoji as text too; emoji is for when the
picture is the whole answer.
An optional Field that never arrives is accepted. An optional Field that arrives wrong is refused — sending nothing and sending rubbish are not the same thing.
Dates, dropdowns and Other answers
A date is a real calendar date in YYYY-MM-DD format, without a time or timezone.
The public page uses the browser's date picker. minDate and maxDate are inclusive;
afterField names a date Field above it and requires this answer to fall on or after
that answer, when the earlier Field's rule is open and it has a value.
[
{ "key": "start", "label": "Start date", "type": "date", "required": false },
{ "key": "end", "label": "End date", "type": "date", "required": false, "afterField": "start" }
]A select displays radio buttons; a dropdown displays a dropdown. Set
searchable: true on a dropdown to let respondents filter its options. In the
builder, paste one option per line; commas inside a line remain part of the option.
Use line breaks between options, including short lists.
Set allowOther: true on a select, dropdown or multiselect to show an Other
choice and a text input. The written answer is stored directly in the Field's value,
not under an extra key. A multiselect may carry its known options and one written
answer. Written answers must be nonblank and at most 500 characters. Without
allowOther, unknown answers are refused as before.
Instructions and page content
Every Field may carry description, at most 4000 characters. Answer Fields display
it as help text; a content Field displays its label as a heading and its
description as a standalone text block between questions. Text blocks have keys
for ordering and conditions, but no answer is stored for them. They cannot be
required, hidden or locked, have defaults, or be the target of a condition.
Descriptions and page text preserve line breaks and accept links written as
[Privacy policy](https://example.com/privacy). HTML is displayed as text.
On a survey, the builder's Public page panel controls the introduction, text
before the submit button, and the confirmation heading and message. The definition
API and JSON import/export carry these in a page object:
{
"description": "Tell us about your group.",
"footer": "By submitting, you agree that we may contact you. [Privacy policy](https://example.com/privacy)",
"confirmationTitle": "Thank you for signing up!",
"confirmationMessage": "We will be in touch within 1–3 business days."
}The Fields CSV also accepts description, searchable, allowOther, minDate,
maxDate and afterField columns. Page settings belong to JSON definitions, not
Fields CSVs.
Defaults, hidden and locked Fields
Any Field may carry a default: a valid value for its type, written to the Submission
when the payload leaves the key out — not sent, null, blank, or an empty list. On a
public page the control starts from it. A required Field with a default is never
missing.
Set hidden: true and the public page shows no input for the Field. Its value comes
from the link — /f/handle?source=newsletter — or from the default, and it is posted
and checked like any other. Hidden is for tracking: a source, a segment, a campaign
per link. Anyone can write a link, so it is not for trust. A hidden Field can only be
required when it has a default, because the respondent cannot fill what they cannot
see.
Set locked: true and the default becomes the Field's only value. Ingest writes it
whatever arrived, a payload that sends a different value is refused with
locked field: key, the public page shows no input for it, and the link cannot change
it. Locked is for trust: a version, a region, anything the respondent must not decide.
A locked Field without a default is refused when the Form is saved.
[
{ "key": "source", "label": "Source", "type": "text", "required": false, "default": "direct", "hidden": true },
{ "key": "version", "label": "Version", "type": "text", "required": false, "default": "2.4", "locked": true }
]The three keys describe the public page. On a feedback Form, whose screen is yours,
hidden changes nothing; default and locked still apply on ingest. All three work
in a CSV import too, and GET /api/v1/form returns them, so a client that renders its
own screen can start from the defaults.
Conditional Fields
A Field may carry a rule, when, that points at a Field above it. The Field shows on
the public page, and is required, only while the rule holds. Choose a and the
follow-ups for a appear; choose b and they close and b's open instead. Several
Fields sharing a rule are a branch. A Field may depend on a conditional Field, so a
branch can have branches, and it closes with its parent.
The simplest case is the feedback widget: a thumb, and a comment box that only appears for a thumb down. A thumb up is a complete Submission on its own.
[
{ "key": "vote", "label": "Was this helpful?", "type": "emoji", "required": true, "options": ["👍", "👎"] },
{ "key": "comment", "label": "What could be better?", "type": "textarea", "required": false, "when": { "key": "vote", "is": ["👎"] } },
{ "key": "rating", "label": "Rating", "type": "scale", "required": false, "min": 1, "max": 10 },
{ "key": "why_low", "label": "What would make it a 10?", "type": "textarea", "required": false, "when": { "key": "rating", "max": 6 } }
]The rule follows the target's type. A select, dropdown, multiselect or emoji target takes
is, a list of its options — any of them matches, and on a multiselect any chosen
option counts. A text-like target takes is too, compared trimmed and ignoring case.
A number, scale or satisfaction target takes min, max or both. A rule may
only point upward: the Form is refused if it points at a Field below, at itself, or
at a value the target cannot have.
A hidden or locked Field can be a target. /f/handle?plan=pro then opens the pro
branch from the link. In a CSV import the column is when, spelled
outcome=badly|fine, rating<=6 or rating>=8.
Ingest owes nothing for a branch not taken: a required Field whose rule does not hold may be left out. A value sent for it anyway is accepted and checked like any other — your own code may know something the rule does not.
Prefill from the link
Any Field that is not locked takes its starting value from the page's query string:
/f/handle?plan=pro&rating=4. A visible Field starts from it and the respondent may
change it; a hidden one carries it. A multiselect takes repeated params
(?tags=api&tags=docs) or one comma-separated value (?tags=api,docs). A value the
Field would refuse is dropped and the default, if any, is used instead — a bad link
must not fail a Submission the respondent cannot fix.
Example: an RSVP
A survey Form for an event: which sessions someone will attend, and — only if one of
them is the meet session — who they want to meet. host is required, but only
while sessions includes meet; tick anything else and it is neither shown nor owed.
[
{ "key": "name", "label": "Your name", "type": "text", "required": true },
{ "key": "email", "label": "Email", "type": "email", "required": true },
{ "key": "sessions", "label": "Which sessions will you attend?", "type": "multiselect", "required": true, "options": ["keynote", "workshop", "meet"] },
{ "key": "host", "label": "Who would you like to meet?", "type": "text", "required": true, "when": { "key": "sessions", "is": ["meet"] } }
]Send each invitee a link with their sessions already ticked:
https://noonmark.dev/f/rsvp?sessions=keynote&sessions=meetThe page opens with keynote and meet chosen, so the host question is already
showing. The invitee can change the choice; untick meet and host closes, and
the Submission is accepted without it. The same Form through the API:
{ "name": "Ana", "email": "ana@acme.com", "sessions": ["meet"] } is refused with
missing field: host, and "sessions": ["keynote"] needs no host at all.
Steps on the survey page
Set step: true on a Field and the survey page starts a new step there. The page shows
one step at a time, with Back and Next buttons, and Next checks only that
step's required Fields. There is one POST, at the end, with every step's answers. The
Submission is the same object it would be from a one-page Form.
[
{ "key": "plan", "label": "Plan", "type": "select", "required": true, "options": ["free", "pro"] },
{ "key": "why_pro", "label": "Why pro?", "type": "text", "required": true, "step": true, "when": { "key": "plan", "is": ["pro"] } },
{ "key": "comment", "label": "Anything else?", "type": "textarea", "required": false, "step": true }
]A step whose Fields are all kept off the page by their rules skips itself. So does a
step that holds only hidden or locked Fields. With plan set to free, the page above
goes straight from the first step to the last. The marker on the first Field means
nothing and is dropped.
In the Studio it is the new step checkbox on a Field. The Fields CSV has a step
column (1, true or yes), and GET /api/v1/form returns the marker with the Fields,
so your own renderer can page the same way. Ingest ignores it.
Validation errors
They all return 400 with the key that failed, so you can fix it without guessing.
missing field: satisfaction
invalid email: email
invalid url: website
locked field: source
invalid number: rating
invalid scale: satisfaction
out of range: satisfaction (1..5)
invalid option: plan
repeated option: features
invalid selection: features
invalid text: commentForm stepper
In a survey's Public page settings, choose Form stepper under Layout to show one visible question at a time. Conditional questions and hidden Fields keep their existing behavior. Back retains answers, and only the final Submit sends a response.
Enter advances text inputs; Shift + Enter adds a line in a long answer. Cmd/Ctrl + Enter advances or submits. The page includes progress and a full-page confirmation using your confirmation title and message. Public forms start in Day mode, with a Day/Night switch at the top.
What did you think of this content?
@noonmark/sdk
A small client with no dependencies. Everything it does you could do with fetch — it exists for the types and the error handling.
Integrations
What a Form can do beyond storing a Submission, in three families: Sources bring Submissions in, Facets are carried by every Submission, and Steps run when something happens to one.