Skip to main content

webiny-form-model

Building forms with the FormModel system — field types, renderers, layout, validation, conditional rules, computed fields, and dynamic zones. Use this skill when the developer needs to define form fields with the builder API, choose renderers, build layouts with tabs/rows/separators, add validation (Zod or imperative), use conditional visibility/disable rules, create computed fields, or work with object fields and templates (dynamic zones).

Source facts

Repository
webiny/webiny-js
Last source activity
July 24, 2026 at 12:20
Detected SKILL.md language
English
Stars
8,049
Forks
682

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
webiny-form-model
description
Building forms with the FormModel system — field types, renderers, layout, validation, conditional rules, computed fields, and dynamic zones. Use this skill when the developer needs to define form fields with the builder API, choose renderers, build layouts with tabs/rows/separators, add validation (Zod or imperative), use conditional visibility/disable rules, create computed fields, or work with object fields and templates (dynamic zones).
# Form Model ## TL;DR The Form Model is Webiny's declarative form system. Define fields with a fluent builder API (`fields.text()`, `fields.datetime()`, etc.), arrange them with a layout builder (`layout.row()`, `layout.tabs()`, etc.), and validate with Zod schemas or imperative rules. Fields support conditional visibility, computed values, reactive context from other fields (`.context()`), and deeply nested object/list structures with templates (dynamic zones). ## Field Types All fields are created via the `fields` registry callback. Each builder method returns a chainable builder. ### Text ```typescript fields.text(); ``` Default renderer: `textInput`. Value: `string | null`. ```typescript fields.text().label("Title").placeholder("Enter title").required("Title is required") fields.text().renderer("textarea", { rows: 4 }) fields.text().list().renderer("tags").defaultValue([]) fields.text().list().renderer("textInputs", { addItemLabel: "Add text" }) fields.text().list().renderer("textareas", { addItemLabel: "Add description" }) fields.text().renderer("codeEditor", { language: "html", height: 300 }) fields.text().options([ { label: "Option A", value: "a" }, { label: "Option B", value: "b" } ]) // auto-switches to "select" renderer fields.text().options([...]).renderer("radioButtons") fields.text().list().options([...]).renderer("checkboxes") // Dynamic options — callback receives { field, form }, re-evaluated reactively fields.text().options(({ form }) => { const type = form.field("general.type").getValue(); return getOptionsForType(type); }) ``` ### Number ```typescript fields.number(); ``` Default renderer: `numberInput`. Value: `number | null`. Auto-normalizes to number. ```typescript fields.number().label("Count").placeholder("0").required(); fields.number().list().renderer("numberInputs", { addItemLabel: "Add number" }); fields.number().options([ { label: "Tier 1", value: 100 }, { label: "Tier 2", value: 200 } ]); ``` ### Boolean ```typescript fields.boolean(); ``` Default renderer: `switch`. Value: `boolean | null`. ```typescript fields.boolean().label("Featured").defaultValue(false); ``` ### DateTime ```typescript fields.datetime(); ``` Default renderer: `dateTimeInput`. Pick a variant method to set the subtype: | Variant | Value Format | Example | | ----------------------------------------- | ------------------------------ | ----------------------- | | `.dateOnly()` | `"2026-05-01"` | Birthdays, due dates | | `.timeOnly()` | `"14:30:00"` | Opening hours | | `.withTimezone()` | `"2026-05-01T14:30:00+02:00"` | Events tied to a locale | | `.withoutTimezone()` | `"2026-05-01T14:30:00.000Z"` | Timestamps, logs | | `.monthOnly()` | `"2026-05"` | Billing cycles | | `.weekOnly({ startsOn: 1 })` | `"2026-W18"` | Sprint planning | | `.yearOnly({ range: [2020, 2035] })` | `2026` (number) | Fiscal years | | `.dateRange()` | `{ from: "...", to: "..." }` | Vacation requests | | `.multipleDates()` | `["2026-05-01", "2026-05-03"]` | Blackout dates | | `.multipleMonths()` | `["2026-01", "2026-03"]` | Seasonal availability | | `.multipleYears({ range: [2020, 2035] })` | `[2024, 2025, 2026]` | Multi-year budgets | Additional chainable methods: ```typescript .presets([ { label: "Today", value: () => new Date() }, { label: "In a week", value: () => addDays(new Date(), 7) } ]) .displayFormat("dd/MM/yyyy") // date-fns format tokens .list() // switches renderer to "dateTimeInputs" ``` ### File ```typescript fields.file(); ``` Default renderer: `filePicker`. Value: `FileValue | null` (object with `id`, `name`, `size`, `mimeType`, `src`, `width`, `height`). ```typescript fields.file().label("Image"); ``` ### File URL ```typescript fields.fileUrl(); ``` Default renderer: `fileUrlPicker`. Value: `string | null` (URL only). ```typescript fields.fileUrl().label("Image URL"); ``` ### Lexical ```typescript fields.lexical(); ``` Default renderer: `lexical`. Value: `RichTextValueWithHtml | null` (`{ state: string; html: string }`). ```typescript fields.lexical().label("Content").required("Content is required"); ``` ### Password ```typescript fields.password(); ``` Default renderer: `passwordInput`. Value: `string | null`. ```typescript fields.password().label("Password").required("Password is required"); ``` ### Permissions ```typescript fields.permissions(); ``` Default renderer: `permissions`. Value: `Record<string, unknown>[]`. Has a built-in Zod schema requiring at least one permission entry. ```typescript fields.permissions().label("Permissions"); ``` ### Roles Multi-Select ```typescript fields.rolesMultiSelect(); ``` Default renderer: `rolesMultiSelect`. Value: `unknown[]`. ```typescript fields.rolesMultiSelect().label("Roles"); ``` ### Object ```typescript fields.object(); ``` Default renderer: `objectAccordionSingle`. For nested structures, lists, and dynamic zones. ```typescript // Simple nested object fields.object().label("Address").fields(f => ({ street: f.text().label("Street"), city: f.text().label("City"), zip: f.text().label("ZIP") })) // List of objects fields.object().list().label("Authors").fields(f => ({ name: f.text().label("Name").required(), email: f.text().label("Email") })) // Dynamic zone (single template selection) fields.object().label("Content Block") .template("hero", t => { t.label("Hero Banner") .icon({ type: "icon", name: "fas/image" }) .fields(f => ({ heading: f.text().label("Heading").required(), image: f.file().label("Image") })); }) .template("text", t => { t.label("Rich Text").fields(f => ({ body: f.text().label("Body").renderer("textarea") })); }) // Dynamic zone list (multiple items, each picks a template) fields.object().list().label("Page Sections") .renderer("dynamicZone", { container: false }) .template("hero", t => { ... }) .template("cta", t => { ... }) // Key-value list fields.object().list().label("Meta Tags") .renderer("keyValueTags", { addItemLabel: "Add tag" }) .fields(f => ({ name: f.text().placeholder("Name"), content: f.text().placeholder("Content") })) ``` Template visibility can be conditional: ```typescript .template("premium", t => { t.label("Premium Widget") .visible(form => form.field("plan").getValue() === "enterprise") .fields(f => ({ ... })); }) ``` ## Common Builder Methods These are available on **all** field types: | Method | Description | | ----------------------------- | ------------------------------------------------------- | | `.label(text)` | Field label | | `.description(text)` | Description text below the field | | `.help(text)` | Help text | | `.note(text)` | Supplementary note | | `.placeholder(text)` | Input placeholder | | `.defaultValue(value)` | Default value (can be a function for dynamic defaults) | | `.required(message?)` | Mark as required | | `.requiredWhen(fn, message?)` | Conditionally required based on other field values | | `.schema(zodSchema)` | Zod validation schema | | `.renderer(name, settings?)` | Override the default renderer | | `.options([...])` | Add value options (auto-switches text/number to select) | | `.list()` | Convert to array field | | `.hidden()` | Hide the field (value still in form data) | | `.hiddenWhen(fn)` | Conditionally hide based on form state | | `.disabled(value?)` | Disable the field | | `.disabledWhen(fn)` | Conditionally disable based on form state | | `.rules([...])` | Conditional visibility/disable rules | | `.computed(fn)` | Always-computed value from other fields | | `.computedUntilDirty(fn)` | Computed until user edits the field |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub