docs

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.

typein the Studiovalue in JSONexampleextra keys
texttextstring"Ana"
textarealong answerstring"Loved the product"
datedateYYYY-MM-DD string"2026-10-01"minDate, maxDate, afterField
dropdowndropdownstring, one of the options or an allowed Other answer"University of Bristol"options, searchable, allowOther
contenttext blockno answer—description
emailemailstring with @"ana@acme.com"
urlurlhttp or https address"https://acme.com/pricing"
numbernumberfinite number5
scalescaleinteger between min and max9min, max
satisfactionsatisfactioninteger 1 to 55minLabel, maxLabel
selectchoicestring, one of the options"pro"options
multiselectmultiple choicestring[], within options["api","webhooks"]options
emojiemojistring, 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.

trip dates
[
  { "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:

page settings
{
  "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.

fields.json
[
  { "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.

fields.json
[
  { "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.

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.

fields.json
[
  { "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:

link
https://noonmark.dev/f/rsvp?sessions=keynote&sessions=meet

The 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.

fields.json
[
  { "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.

errors
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: comment

Form 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?

On this page