Building forms
Field reference
Every field type form2wf renders, with a sample and the data it submits.
Common properties#
| Property | Description |
|---|---|
id | Unique id within the form. Required for buttons and row actions to work — always set one (the dashboard generates field_… ids). |
type | One of the types below. |
name | Key in the submitted data. |
label | Label shown above the input. |
placeholder | Placeholder text. For subtitle it is the heading text; for checkbox, the text beside the box. |
required | Blocks submission until filled (browser-side check). |
colSpan | How many grid columns the field takes, from 1 (default) up to the form's columns, or "full" for the whole row. See multi-column layouts. |
newRow | true starts the field on a new row. |
colStart | Column the field starts in (1 = new row). If earlier fields on the row already use that column, the field moves to the next row. |
defaultValue | Initial value. Supports templates such as {{params.plan}}. |
Text inputs: text, email, number, tel, date, url, password#
Rendered as <input type="…">. Any HTML input type works — the type is passed through — and browsers apply their native validation for email, url and number. All values are submitted as strings.
{ "id": "phone", "type": "tel", "name": "phone", "label": "Phone", "placeholder": "+1 555 0100" }
{ "id": "start", "type": "date", "name": "start_date", "label": "Start date", "required": true }
{ "id": "qty", "type": "number", "name": "quantity", "label": "Quantity", "defaultValue": "1" }
{ "phone": "+1 555 0100", "start_date": "2026-10-01", "quantity": "1" }textarea#
{ "id": "msg", "type": "textarea", "name": "message", "label": "Message", "required": true, "placeholder": "How can we help?" }
select#
A dropdown whose first entry is an empty “Select an option”. Each option's value equals its label. defaultValue selects an exact match.
{ "id": "plan", "type": "select", "name": "plan", "label": "Plan",
"options": ["Starter", "Pro", "Enterprise"], "defaultValue": "{{params.plan}}" }
radio#
{ "id": "contact", "type": "radio", "name": "contact_by", "label": "Contact me by",
"options": ["Email", "Phone"], "required": true }
rating#
Star rating. Visitors click or drag across the stars, or use the arrow keys (Home or Delete clears it). The value is submitted as a number, such as 3.75; an unrated optional field submits an empty string.
{ "id": "score", "type": "rating", "name": "score", "label": "How was your visit?", "required": true,
"ratingConfig": { "max": 5, "step": 0.25 } }
ratingConfig key | Values | Default |
|---|---|---|
max | Number of stars, 1–10 | 5 |
step | 0.25 (quarter stars), 0.5 or 1 | 0.25 |
defaultValue sets the starting rating and supports templates, e.g. {{data.score}} to show a stored rating on a data page.
checkbox#
A single checkbox. Checked submits "true"; unchecked is omitted entirely, so treat a missing key as false in n8n. defaultValue of "true" starts it checked.
{ "id": "tos", "type": "checkbox", "name": "accept_terms", "label": "Terms",
"placeholder": "I agree to the terms of service", "required": true }
{ "accept_terms": "true" }file#
A drag-and-drop zone for one file. When a file is attached the submission is sent as multipart/form-data instead of JSON — see multipart payloads. Limit: 10 MB per file.
{ "id": "cv", "type": "file", "name": "resume", "label": "Résumé (PDF)", "required": true }
Multipart submissions only carry top-level text values. Don't combine file with object or array fields in the same form — nested values are dropped.
hidden#
Sends a value the visitor doesn't see — ideal for tracking the source page or a record id passed in the URL.
{ "id": "src", "type": "hidden", "name": "source", "defaultValue": "{{params.utm_source}}" }
display#
Read-only text, usually filled from the onload response. Rendered as plain text (HTML is not interpreted).
{ "id": "greet", "type": "display", "label": "Account", "displayValue": "Signed in as {{data.user.name}} ({{data.user.plan}})" }
Layout: subtitle, divider, linebreak#
{ "id": "h1", "type": "subtitle", "label": "Shipping address" }
{ "id": "hr", "type": "divider" }
{ "id": "br", "type": "linebreak" }
subtitle renders a section heading, divider a horizontal rule, and linebreak ends the current row in a multi-column layout.
object#
Groups child fields into a nested object. Optionally collapsible, with a preview of chosen child values while collapsed.
{
"id": "addr", "type": "object", "name": "address", "label": "Address",
"objectConfig": { "collapsible": true, "defaultCollapsed": false, "collapsedPreviewFields": ["city"] },
"children": [
{ "id": "street", "type": "text", "name": "street", "label": "Street" },
{ "id": "city", "type": "text", "name": "city", "label": "City", "required": true },
{ "id": "zip", "type": "text", "name": "zip", "label": "Postal code" }
]
}
{ "address": { "street": "1 Main St", "city": "Oslo", "zip": "0150" } }array#
A repeatable group: visitors add, remove and drag-reorder items. Always shows at least one item.
arrayConfig | Default | Description |
|---|---|---|
minItems | 0 | Items can't be removed below this. |
maxItems | unlimited | The add button stops at this count. |
itemLabel | "Item {{index}}" | Heading for each item; {{index}} is 1-based. |
addButtonLabel | "Add Item" | Text of the add button. |
allowReorder | true | Drag handles to reorder items. |
collapsedPreviewFields | [] | Child names shown when an item is collapsed. |
{
"id": "people", "type": "array", "name": "attendees", "label": "Attendees",
"arrayConfig": { "minItems": 1, "maxItems": 10, "itemLabel": "Attendee {{index}}", "addButtonLabel": "Add attendee" },
"children": [
{ "id": "pname", "type": "text", "name": "name", "label": "Name", "required": true },
{ "id": "pemail", "type": "email", "name": "email", "label": "Email" }
]
}
{ "attendees": [ { "name": "Ada", "email": "ada@example.com" }, { "name": "Grace", "email": "" } ] }table#
Shows rows from the onload response, with optional row actions, a search box (tableSearch) and pages (pageSize). See Data pages.
{ "id": "orders", "type": "table", "label": "Recent orders", "dataSource": "orders",
"tableSearch": true, "pageSize": 20,
"columns": [ { "field": "number", "label": "Order" }, { "field": "total", "label": "Total" } ] }
markdown#
Formatted text across the full width of the form: headings, paragraphs, bold and italic, lists and task lists, tables, quotes, code blocks, links and images. Use it for instructions, terms, a welcome message, or an answer your workflow writes (an AI summary, an article, a status report). It submits nothing.
{ "id": "intro", "type": "markdown",
"markdown": "## Welcome, {{data.user.name}}\n\nUpload your **2026 receipts** below. We'll email you when we're done." }
Or take the text from the onload response with dataSource, a plain dot path to a markdown string:
{ "id": "answer", "type": "markdown", "label": "Summary", "dataSource": "summary", "maxHeight": 400 }
{ "data": { "summary": "### Contract review\n\n- **Term:** 24 months\n- **Risk:** auto-renewal clause (section 9)" } }| Property | Meaning |
|---|---|
markdown | The text. Supports templates ({{data.x}}, {{params.x}}). |
dataSource | Path to a markdown string in the onload data. When it has text, it's shown instead of markdown. |
maxHeight | Pixels (80+). Taller content scrolls inside the field. |
label | Optional heading above the text. |
HTML inside the markdown is shown as text and never runs. Links may only be https, http, mailto, tel or #anchors, and open in a new tab; images must be https or http. That makes it safe to show text that came from visitors or from an AI model.
nav (menu bar)#
A full-width menu that turns a set of forms into an app: each item opens one of your forms in place (or an external page in a new tab), and the item for the form being shown is highlighted. Put the same menu at the top of each form in the app.
{ "id": "menu", "type": "nav", "navItems": [
{ "label": "Dashboard", "form": "portal-home" },
{ "label": "My requests", "form": "portal-requests", "params": { "status": "open" } },
{ "label": "Help", "url": "https://example.com/help" }
] }
form is a form slug in your organization; params are passed like a navigate link's ({{params.x}} in the target form, and in its onload webhook). In the builder, add a Menu Bar element and write one item per line: Label | form-slug or Label | https://….
chart#
Draws data from the onload response as a bar, line, area, pie or donut chart, or as a single headline number (indicator). Charts take the width their column span gives them and redraw when it changes, so they fit a quarter-width cell as well as a full row. Visitors can hover or use the arrow keys for exact values, and a "Show table" link lists every value. Charts submit nothing.
{ "id": "sales", "type": "chart", "label": "Sales by month", "colSpan": 2,
"chartConfig": { "kind": "bar", "dataSource": "sales", "labelKey": "month",
"series": [ { "key": "online", "label": "Online" }, { "key": "store", "label": "In store" } ],
"stacked": true, "format": "currency", "currency": "USD" } }
{ "data": { "sales": [ { "month": "Jan", "online": 12400, "store": 8300 },
{ "month": "Feb", "online": 13900, "store": 7900 } ] } }chartConfig key | Applies to | Values |
|---|---|---|
kind | all | bar (default), line, area, pie, donut, indicator |
dataSource | all | Dot path to an array in the onload data, like sales (not {{data.sales}}). Each row is one bar, point or slice. |
labelKey | bar, line, area, pie, donut | Row key for the category or slice name. Default: the first text key. |
series | all | Number keys to plot: ["online"] or [{ "key": "online", "label": "Online" }]. Several series make a grouped or stacked bar chart or a multi-line chart. Default: every number key. Pie and donut use the first; indicator uses it for its trend line. |
data | all | Static rows instead of dataSource, for a chart that never changes. |
horizontal, stacked | bar | true for horizontal bars or stacked series. |
valueLabels | bar | Values at the bar ends (one series, or stacked totals). Default true. |
smooth, markers | line, area | smooth: true curves the line. markers: true or false; by default dots show for up to 12 points. |
sliceLabels | pie, donut | percent (default), value or none. Labels only appear where they fit. |
centerLabel | donut | Text under the total in the hole. Default Total. |
value | indicator | A template such as {{data.visitors}}. Empty = the last row of dataSource. |
compareTo, compareLabel, goodWhen | indicator | Shows the change as a percentage: compareTo is a template or previous (the row before the last). compareLabel e.g. vs last month. goodWhen: up (default, increases are green), down or none. |
caption | indicator | A short line under the number. |
format | all | auto (default: 1610.01 → 1.6K, 99.6912 → 99.7), compact (1.6K), number (1,610.01), percent (fractions: 0.997 → 99.7%), currency ($1,610.01; $48.2K from 10,000). Axes, labels and tooltips use it too. |
decimals, currency, prefix, suffix | all | Fixed decimal places (default automatic); ISO currency code (default USD); text before or after every number, e.g. "suffix": " kg". For values already in 0–100, use "suffix": "%" instead of percent. |
palette, colors | all | theme (default: your theme's primary colour first), default, vivid, or custom with colors: ["#2a78d6", "#eb6834"]. Default and vivid are tested to stay distinguishable with colour blindness. Extra series beyond the palette are not drawn; extra pie slices are combined into "Other". |
height | bar, line, area, pie, donut | Pixels, 120–800. Defaults: 260, pie and donut 240, horizontal bars grow with the rows. |
table | bar, line, area, pie, donut | false hides the "Show table" link. |
{ "id": "visitors", "type": "chart", "label": "Visitors",
"chartConfig": { "kind": "indicator", "value": "{{data.visitors}}", "compareTo": "{{data.visitorsLastMonth}}",
"compareLabel": "vs last month" } }button#
Triggers a webhook, opens another form, opens a web page, or goes back. Consecutive buttons and links render in one row. See Buttons.
{ "id": "approve", "type": "button", "label": "Approve", "buttonAction": "webhook", "buttonStyle": "primary",
"buttonWebhookUrl": "https://n8n.example.com/webhook/approve?id={{params.id}}", "buttonWebhookMethod": "POST" }
{ "id": "open", "type": "button", "label": "Open", "buttonAction": "url",
"buttonUrl": "https://files.example.com/{{data.file.path}}" }
A url button opens buttonUrl in a new tab, like a link with linkAction: "url". Templates work in it; only https, http, mailto and tel URLs open.
link#
{ "id": "help", "type": "link", "label": "Pricing details", "linkAction": "url", "linkUrl": "https://example.com/pricing?plan={{params.plan}}" }
{ "id": "next", "type": "link", "label": "Continue to step 2", "linkAction": "navigate",
"linkTargetForm": "signup-step-2", "linkTargetParams": { "email": "{{params.email}}" } }
linkAction: navigate (default — load another form of yours by slug), back, or url (opens in a new tab).
Not supported#
Conditional visibility, min/max/pattern validation rules, multi-select dropdowns, and options with separate values and labels. Validate important rules in your n8n workflow.