Skip to main content
Forms are how Prefab collect structured data from users. You can build forms by hand with individual input components, or generate them automatically from a Pydantic model with Form.from_model().

Building Forms by Hand

Combine Input, Label, and Button components to create a form with full control over layout and behavior. Named inputs automatically sync their values to client state, so {{ name }} interpolation references the current value of the input with name="name". This approach gives you complete control, but it requires wiring up every label, input type, and argument template yourself. For models with many fields, Form.from_model() handles all of that automatically.

Generating Forms from Pydantic Models

Form.from_model() introspects a Pydantic model and generates a complete form — labels, inputs, validation constraints, and submit button — from the model’s field definitions. Each field in the model becomes a labeled input. The field’s type determines what kind of input is rendered, and Pydantic Field() metadata controls labels, placeholders, and constraints.

Metadata Mapping

Pydantic Field() parameters map directly to form behavior:
Field metadataForm effect
Field(title="...")Label text (fallback: humanized field name)
Field(description="...")Placeholder text
Field(min_length=..., max_length=...)HTML input constraints
Field(ge=..., le=...)Number input min/max
Field(json_schema_extra={"ui": {"type": "textarea", "rows": 4}})Textarea override
Field(exclude=True)Skip field entirely

Type Mapping

The field’s Python type determines the input component:
Python typeInput component
strText input (auto-detects email, password, tel, url from field name)
int, floatNumber input
boolCheckbox
Literal["a", "b", "c"]Select dropdown
SecretStrPassword input
datetime.dateDate picker
datetime.timeTime picker
datetime.datetimeDatetime picker

Auto-Fill Arguments

When on_submit is a ToolCall with no arguments, from_model() auto-generates the arguments from the model’s fields, wrapped under a data key.
Auto-Fill
from pydantic import BaseModel, Field
from prefab_ui.components import Form
from prefab_ui import ToolCall

class ContactInfo(BaseModel):
    name: str = Field(title="Full Name", min_length=1)
    email: str
    message: str

Form.from_model(ContactInfo, on_submit=ToolCall("submit_contact"))
The generated ToolCall is equivalent to writing this explicitly:
ToolCall("submit_contact", arguments={
    "data": {
        "name": "{{ name }}",
        "email": "{{ email }}",
        "message": "{{ message }}",
    }
})
This convention enables a powerful pattern: a tool that both displays the form and processes the submission.

Self-Calling Tool Pattern

A single tool can serve as both the form renderer and the form handler. On the first call (no data), it returns the form. When the user submits, the auto-filled arguments pass the form values back to the same tool as a validated Pydantic model.
Self-Calling Tool
from pydantic import BaseModel, Field
from prefab_ui import FastMCP
from prefab_ui import AppResult, ToolCall, ShowToast
from prefab_ui.components import Form, Text

mcp = FastMCP("Contacts")

class Contact(BaseModel):
    name: str = Field(min_length=1)
    email: str
    phone: str = ""

@mcp.tool(ui=True)
async def create_contact(data: Contact | None = None) -> AppResult:
    if data is None:
        return AppResult(
            view=Form.from_model(
                Contact,
                on_submit=ToolCall(
                    "create_contact",
                    on_success=ShowToast("Saved!"),
                ),
            ),
        )
    save_to_db(data)
    return AppResult(view=Text(f"Created contact: {data.name}"))
When data is None (the initial call), the tool returns a form. When the user fills in the fields and clicks submit, the renderer calls create_contact again with data populated from the form values. Pydantic validates the input automatically, so data arrives as a fully validated Contact instance.

Error Handling

When on_submit is auto-filled and no on_error is specified, from_model() adds a default error toast:
ShowToast("{{ $error }}", variant="error")
The $error variable captures the error message from a failed tool call, making validation errors visible to the user without any extra configuration. You can override this with your own on_error:
Custom Error Handling
from pydantic import BaseModel, Field
from prefab_ui.components import Form
from prefab_ui import ToolCall, ShowToast

class Contact(BaseModel):
    name: str = Field(min_length=1)
    email: str

Form.from_model(
    Contact,
    on_submit=ToolCall(
        "create_contact",
        on_error=ShowToast("Could not save: {{ $error }}", variant="error"),
    ),
)

Unsupported Types

from_model() skips fields with complex types that have no natural form input mapping: list, dict, set, tuple, and nested BaseModel instances. If you need these, build those parts of the form manually and handle the arguments yourself.