# Form authoring guide Forms in vibe_erp describe how an entity is displayed and edited. They are **data, not code** — stored as `metadata__form` rows, consumed by one renderer, and used identically inside and outside workflows. For where forms sit in the architecture, see [`../architecture/overview.md`](../architecture/overview.md). For the workflow side, see the [workflow authoring guide](../workflow-authoring/guide.md). ## JSON Schema + UI Schema A vibe_erp form definition has two halves: - **JSON Schema** describes the **data shape**: which fields exist, their types, which are required, what their validation constraints are. - **UI Schema** describes the **layout and widgets**: which field renders as a dropdown vs. a radio group, which fields are grouped under which tab, which fields are read-only, which are conditionally visible. The split matters. The JSON Schema is the contract — server-side validation, OpenAPI generation, and the AI-agent function catalog all read it. The UI Schema is presentation — only the renderer reads it. Two forms can share a JSON Schema but render differently. A form definition looks roughly like this (illustrative): ```json { "id": "orders_sales.order.create", "schema": { "type": "object", "required": ["customerId", "lines"], "properties": { "customerId": { "type": "string", "format": "uuid" }, "deliveryDate": { "type": "string", "format": "date" }, "lines": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["itemId", "quantity"], "properties": { "itemId": { "type": "string", "format": "uuid" }, "quantity": { "type": "number", "exclusiveMinimum": 0 } } } } } }, "uiSchema": { "type": "VerticalLayout", "elements": [ { "type": "Control", "scope": "#/properties/customerId", "options": { "widget": "partner-picker" } }, { "type": "Control", "scope": "#/properties/deliveryDate" }, { "type": "Control", "scope": "#/properties/lines", "options": { "widget": "line-items-grid" } } ] } } ``` ## Two authoring paths Both paths are first-class. The renderer does not know which one produced a given form. ### Tier 1 — Form designer in the web UI (v1.0) Business analysts build forms in the form designer that ships in the web SPA. The result is stored as a row in `metadata__form`, scoped to the tenant, tagged `source = 'user'` so it survives plug-in install/uninstall and core upgrades. No build, no restart. This path is the right one for: - Tweaking the layout of an existing entity for a specific tenant. - Adding a tenant-specific custom field to an existing form. - Building a form for a Tier 1 custom entity. The form designer is a **v1.0 deliverable**. ### Tier 2 — JSON files in a plug-in JAR (v0.1) Plug-in authors ship form definitions inside their JAR under `src/main/resources/metadata/forms/`. The plug-in lifecycle upserts them into `metadata__form` on plug-in install, tagged `source = 'plugin:'`. ``` src/main/resources/ ├── plugin.yml └── metadata/ └── forms/ ├── plate_spec.json └── job_card_review.json ``` ```yaml # plugin.yml metadata: forms: - metadata/forms/plate_spec.json - metadata/forms/job_card_review.json ``` This path is the right one for forms that ship as part of a vertical plug-in (the printing-shop plug-in is the canonical example). ## Forms can reference custom fields A form can reference any custom field defined for the same entity by its key. Because custom fields live as JSON keys in the entity's `ext` JSONB column and are described by `metadata__custom_field` rows, the form designer and the form renderer both already know about them — no additional wiring required. The key is the same one the customer chose when creating the custom field. If a printing-shop integrator added a `coating_finish` custom field to the `production.work_order` entity, a form referencing `ext.coating_finish` will render and validate it correctly, whether the form was authored through the Tier 1 designer or shipped inside a plug-in JAR. ## Validation runs on both sides Validation happens in two places, and both come from the same JSON Schema: - **Client-side** in the renderer, driven by the UI Schema's widgets and the JSON Schema constraints. This is the responsive, immediate-feedback layer. - **Server-side** in the host, driven by the JSON Schema. This is the **authoritative** layer. The renderer's checks are a convenience; the server's checks are the contract. A form submission that bypasses the UI (a script, an integration, an AI agent calling through MCP) is validated against exactly the same JSON Schema as a form filled in through the SPA. There is no path that skips validation. ## Where to go next - Workflows that render forms in user tasks: [`../workflow-authoring/guide.md`](../workflow-authoring/guide.md) - Plug-in author walkthrough: [`../plugin-author/getting-started.md`](../plugin-author/getting-started.md) - Plug-in API surface, including `api.v1.form`: [`../plugin-api/overview.md`](../plugin-api/overview.md)