guide.md 5.11 KB

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. For the workflow side, see the workflow authoring guide.

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):

{
  "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:<id>'.

src/main/resources/
├── plugin.yml
└── metadata/
    └── forms/
        ├── plate_spec.json
        └── job_card_review.json
# 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