Skip to content
Content
Skill

/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

BOOST
From plugin
webiny-js
8k76 skills3 MCP
Install
$ npx -y skills add webiny/webiny-js --skill form-model --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/form-model

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

form-model.SKILL.md
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

fields.text();

Default renderer: `textInput`. Value: `string | null`.

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

fields.number();

Default renderer: `numberInput`. Value: `number | null`. Auto-normalizes to number.

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

fields.boolean();

Default renderer: `switch`. Value: `boolean | null`.

fields.boolean().label("Featured").defaultValue(false);

DateTime

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:

.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

fields.file();

Default renderer: `filePicker`. Value: `FileValue | null` (object with `id`, `name`, `size`, `mimeType`, `src`, `width`, `height`).

fields.file().label("Image");

File URL

fields.fileUrl();

Default renderer: `fileUrlPicker`. Value: `string | null` (URL only).

fields.fileUrl().label("Image URL");

Lexical

fields.lexical();

Default renderer: `lexical`. Value: `RichTextValueWithHtml | null` (`{ state: string; html: string }`).

fields.lexical().label("Content").required("Content is required");

Password

fields.password();

Default renderer: `passwordInput`. Value: `string | null`.

fields.password().label("Password").required("Password is required");

Permissions

fields.permissions();

Default renderer: `permissions`. Value: `Record<string, unknown>[]`. Has a built-in Zod schema requiring at least one permission entry.

fields.permissions().label("Permissions");

Roles Multi-Select

fields.rolesMultiSelect();

Default renderer: `rolesMultiSelect`. Value: `unknown[]`.

fields.rolesMultiSelect().label("Roles");

Object

fields.object();

Default renderer: `objectAccordionSingle`. For nested structures, lists, and dynamic zones.

// Simple nested object
fields.object().label("Address").fields(f => ({
    street: f.text().label("
Read more
Ships withwebiny-js

Open-source content platform. Self-hosted on AWS serverless. Built as a TypeScript framework you extend with code, not a closed product you configure through a UI. Runs on Lambda, DynamoDB, S3, and CloudFront inside your own AWS account. Scales automatically.

Get the whole plugin
Stats
8,049
Stars
682
Forks
Active
Maintenance
TypeScript
Language
16h ago
Last commit
8y ago
Created
2d ago
Added

Repo: webiny/webiny-js

Other skills on webiny-js.