# Input schema reference

> Complete reference for authoring schemas/input.json: which JSON Schema keywords the run form honours, every x-ui key with accepted values and effects, worked widget examples, validation rules and a pre-publish checklist.

Source: https://qsolve.omlabs.co/docs/build/input-schema-reference
Updated: 2026-11-13

`schemas/input.json` does two jobs. It is the contract Qsolve validates every run's input against, and it is the blueprint the run form is drawn from. This page is the exact reference for the second job: what each JSON Schema type and keyword turns into on screen, every `x-ui` key, and where the form and the server differ. For the rules that apply to both schemas (draft-07, no `pattern`, local `$ref` only, 256 KB) and for output schemas, see [Input and output schemas](/docs/build/input-and-output-schemas).

Everything below was checked against the form's source (`schema.ts`, `schema-form.tsx`, `widgets.tsx`, `validate.ts`), the SDK (`runner.py`, `lint.py`, `manifest.py`) and the API (`runs.py`). Where behaviour is surprising it is called out as a note. Anything not verified is marked **unverified**.

## How the pieces fit

```
schemas/input.json  --publish-->  stored with each property's order (x-order)
        |                                  |
        |                         run form is built from it
        |                         (types, keywords, x-ui)
        v
 form checks the input in the browser (Ajv)  --->  POST /v1/runs
                                                        |
                                    server checks again (jsonschema) -> 422 or run starts
                                                        |
                                    SDK checks again before __init__ (jsonschema)
```

- The schema (standard JSON Schema) decides what is valid. It is the only thing that can reject an input.
- `x-ui` only changes how a field is drawn. The server and the SDK ignore it. In the browser form a few `x-ui` rules (`max_from`, `symmetric`, `square`) are also checked before Run, as listed under [Validation](#validation-form-vs-server).
- The form starts every field at its schema `default` and sends it (see [Defaults](#defaults)). Besides the schema, the form itself checks `max_from`, `symmetric`, `square` and the dataset limits, and blocks Run with an inline message; the server does not know these rules.

## Quick start

A complete, minimal input schema with one of each common control:

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["labels", "Q"],
  "additionalProperties": false,
  "properties": {
    "labels": {
      "title": "Variable names",
      "description": "One name per variable",
      "type": "array", "minItems": 2, "maxItems": 12,
      "items": {"type": "string", "minLength": 1, "maxLength": 32}
    },
    "Q": {
      "title": "Q matrix",
      "description": "Paste it from a spreadsheet",
      "type": "array",
      "items": {"type": "array", "items": {"type": "number"}},
      "x-ui": {"widget": "matrix", "labels_from": "labels", "symmetric": true}
    },
    "cardinality": {"title": "Number of ones", "type": "integer", "minimum": 0},
    "mode": {"title": "Mode", "type": "string", "enum": ["fast", "accurate"], "default": "fast"},
    "offset": {"title": "Constant offset", "type": "number", "default": 0, "x-ui": {"advanced": true}}
  }
}
```

The user sees: a chip box for the variable names (type a name and press Enter, or paste a list); a grid whose rows and columns are those names and that stays symmetric; a whole-number box; a two-option switch (fast / accurate); and, behind a "More options" link, the offset box.

Rules of thumb:

1. Put `"additionalProperties": false` on every object so a misspelled field is an error, not silently ignored.
2. Give every property a `title` and a `description`. The title is the label; the description appears in a small info popover next to the label.
3. Declare bounds (`minItems`, `maxItems`, `minimum`, `maximum`, `maxLength`) so mistakes are caught before a run is created.
4. Add at least one example input (`inputs.examples` in the manifest). The run page opens pre-filled with the first one.

## Common behaviour of every field

| Aspect | What happens |
|---|---|
| Label | `title`. With no title, the property name with `_` and `-` turned into spaces and the first letter capitalised (`num_to_select` becomes "Num to select"). Names of two characters or fewer are left as written. |
| Help text | `description` appears in an info popover next to the label, not under the field. Keep it short and put units there. |
| "optional" tag | Shown next to the label of every property not in the object's `required` list. Inside an `anyOf` mode (see [Alternatives](#anyof-alternatives-modes)), the fields of the selected mode count as required. |
| Order | The order you wrote the properties in. The platform records it as `x-order` at publish because the database sorts object keys. Do not write `x-order` yourself: it is overwritten at publish. |
| Errors | Shown in red under the field and linked to the control with `aria-describedby` (the drop-down list for long enums is the one exception). Before the user presses Run the first time, "This is required" messages and top-level errors are hidden; after that all errors show. Arrays show the first error found anywhere inside them; objects show only errors on the object itself, their fields show their own. |
| `$ref` | A local `#/definitions/...` reference is followed; each path segment is decoded as a JSON Pointer (`~1` is `/`, `~0` is `~`, `%20` is a space). Keywords written next to the `$ref` (title, description, x-ui) override the target's; `x-ui` objects are merged key by key, with the sibling winning. A reference loop stops after 16 steps. |
| Unknown shape | A property the form cannot draw (see [Fallbacks](#fallback-a-json-box)) becomes a small JSON text box, so every valid schema stays usable. |
| Keywords the form ignores | `format`, `const`, `allOf`, `oneOf`, `not`, `if/then/else`, `dependencies`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf`, `uniqueItems`, `minLength` (except for the message), `additionalItems`, `contains`, `propertyNames`. The server still enforces the ones the validators support (not `format`, see [Validation](#validation-form-vs-server)). |

## Properties by JSON Schema type

### string

| Keyword / shape | Honoured | Control and behaviour |
|---|---|---|
| plain `type: "string"` | yes | Single-line text box. |
| `enum` (up to 5 values) | yes | Segmented buttons (one choice, no default selected unless the value is set). |
| `enum` (6 or more values) | yes | Drop-down list. |
| `x-ui.option_labels` | yes | Maps each enum value to a display label; the stored value is unchanged. |
| `x-ui.widget: "code"` or `maxLength` above 200 | yes | Multi-line monospace text area, spell-check off, grows from 4 to 10 rows. |
| `x-ui.placeholder` | yes | Placeholder text in the text box or text area. Not used for numbers or enums. |
| `examples` | yes | Clickable chips under a single-line text box. Clicking one sets the value. Not shown for enums, code widgets or long (over 200) strings. |
| `maxLength` | indirect | Switches to the text area above 200. Violations are reported as "Keep this under N characters." No live counter. |
| `minLength` | indirect | Violations are reported as "This can't be empty." for `minLength` 1 and "Use at least N characters." for N above 1. |
| `default` | yes | The form starts with the default typed into the box (a real value, sent with the run). A required enum with no default starts unselected. |
| `format`, `pattern` | no | `pattern` is banned by lint. `format` is ignored by the form and the server. |

Empty text: for an optional field, clearing the box removes the key from the input. For a required field the key stays as `""` (so `minLength: 1` then reports the error).

`type: ["string", "null"]` draws as a string. The form can never produce `null`.

`type: ["string", "number"]` (or `"integer"`) draws as a text box that accepts a number or text: text that reads as a number and prints back identically (`12`, `1.5`, not `007` or `1.0`) is stored as a number, anything else as a string. With `"integer"` and no `"number"`, decimals stay text.

### integer and number

| Keyword / shape | Honoured | Control and behaviour |
|---|---|---|
| `type: "number"` | yes | Number box. The user can type freely (`0.`, `-`, `1e-3`); only valid numbers are stored. Typing text that is not a number leaves the stored value unchanged. |
| `type: "integer"` | yes | Same box; typed decimals are rounded to the nearest whole number (`2.5` becomes `3`). |
| `minimum`, `maximum` | yes | A value outside the range turns the box border red as soon as it is typed; the value is kept, not clamped. A hint next to the box shows only the bounds that exist: "1 to 5", "at least 1" or "at most 5". The error: "Must be at least N." / "Must be at most N." |
| `default` | yes | Starts in the box as a real value and is sent. If the user clears the box the key is removed and the default is shown greyed as a placeholder (multiplied by 100 with `percent`). |
| `x-ui.slider` | yes | Adds a slider, see [slider](#slider). |
| `x-ui.percent` | yes | Shows and accepts percent, see [percent](#percent). |
| `x-ui.max_from` | yes | Shown as a hint and checked by the form. See [max_from](#max_from). |
| `enum` on a number | no | An enum of numbers is drawn as a plain number box. Use a string enum with `option_labels`, or `minimum`/`maximum`. (Solver settings in the run composer are different: a numeric enum choice is sent as the number.) |
| `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` | no | Enforced as a generic error on Run, with no hint on screen. State the rule in the `description`. |

Clearing the box removes the key. `type: ["number", "null"]` draws as a number; `null` cannot be entered.

### boolean

A switch. With a `default` the form starts with that value written; without one the switch shows off but the key is not written until the user flips it, so read it with `input.get("flag", False)`.

### array

The form picks a control by looking at the shape of `items`, in this order. The first match wins.

| Order | Shape | Control |
|---|---|---|
| 1 | `x-ui.widget: "table"` with `x-ui.columns` | [Table](#table) of fixed-length rows |
| 2 | array of arrays of `number` or `integer` (or of untyped items with `x-ui.widget: "matrix"`) | [Matrix grid](#matrix); integer cells round what is typed or pasted |
| 3 | array of `number` or `integer` with `x-ui.labels_from`, or with `x-ui.widget: "vector"` | [Vector](#vector): one box per label (with the widget and no labels: numbered boxes, with Add value / Remove last unless `minItems` equals `maxItems`) |
| 3b | array of `number`/`integer`, `minItems` equal to `maxItems`, at most 4 | Fixed row of boxes labelled x, y, z, w |
| 3c | other array of `number`/`integer` | Number chips ("Type a number and press Enter") |
| 4 | array of strings with `items.enum` | Toggle buttons, pick any number; `option_labels` supported |
| 5 | array of strings | Text chips ("Type a name and press Enter, or paste a list") |
| 6 | array of objects | A card per item with an Add button and a remove cross |
| 7 | anything else (arrays of arrays of strings, arrays of booleans, tuple-form `items: [...]`) | [JSON box](#fallback-a-json-box) |

Keywords on arrays:

| Keyword | Effect |
|---|---|
| `minItems` | Not drawn as a hint; reported as "Add at least N." on Run. Matrix grids stop offering row removal at `minItems` (1 if unset). |
| `maxItems` | Chips stop accepting input at the limit and cut pasted lists to it. Toggle buttons disable unselected options at the limit. Cards and matrices hide the Add button at the limit. A table or a pasted block can still exceed it; the error "At most N allowed." appears on Run. |
| `uniqueItems` | Not enforced by the form (the server enforces it). String chips silently drop duplicates; number chips keep them. Toggle buttons are unique by construction. |
| `default` | Written into the input at start (a copy). Cards: new items start from the item schema's defaults and required fields. |

Chip boxes split what you type or paste on commas, tabs and new lines. A string containing a comma cannot be entered as one chip. Pasting a column from a spreadsheet adds one chip per cell. A trailing `%` on a number chip divides by 100.

### object

| Shape | Control |
|---|---|
| `x-ui.widget: "dataset"`, or an object with properties `X` and `y` | [Dataset editor](#dataset) |
| no `properties`, `additionalProperties` is a schema | A map: [`keys_from`](#additionalproperties-map-keys_from) grid of numbers, or an editable list of named entries |
| has `properties` and is required | The fields drawn inside a shaded panel |
| has `properties` and is optional and absent | A dashed "Set ..." button (using the title in lower case). Pressing it creates the object from its `default`, or from its required fields. An "Use default" link removes it again |
| top-level object | The fields drawn as the form |
| `anyOf` with `required` lists | [Alternatives](#anyof-alternatives-modes) |
| `additionalProperties: true` or omitted, and no `properties` | [JSON box](#fallback-a-json-box) |

An object with both `properties` and a schema in `additionalProperties` shows only the named properties; the extra keys cannot be edited in the form.

### Fallback: a JSON box

Any value the form cannot draw becomes a text area holding raw JSON. It parses as the user types and marks itself red when the JSON is invalid (the last valid value stays in the input). Properties with only `anyOf`/`oneOf`/`allOf`, `type` lists of several non-null types other than string with number (for example `["string", "boolean"]`, drawn as a string), and `type`-less schemas end up here or are mis-drawn. Prefer one concrete `type` per property.

## `x-ui` reference

`x-ui` is an object on a property (or on an `anyOf` entry for `also`). The validators and `qsolve lint` do not look at it: a wrong value never rejects an input and lint does not warn. A key used on the wrong shape is silently ignored.

| Key | Type | Applies to | Default | Exact effect |
|---|---|---|---|---|
| `widget` | `"matrix"`, `"vector"`, `"table"`, `"dataset"`, `"code"` | see rows below | none | An explicit hint wins over the shape guess. `matrix` and `vector` also work when the cell or item type is not declared. Other values are ignored. |
| `widget: "matrix"` | | array of arrays | | Grid of number boxes. Also the automatic choice for any array of arrays of `number`/`integer`. |
| `widget: "vector"` | | array of numbers | | One box per value. With `labels_from` the boxes carry the names; without it they are numbered and the list can grow (Add value) unless `minItems` equals `maxItems`. |
| `widget: "table"` | | array of arrays | | Needs `columns`. Without `columns` it is ignored. |
| `widget: "dataset"` | | object | | Draws the dataset editor. Also applied automatically to any object with `X` and `y` properties. |
| `widget: "code"` | | string | | Multi-line monospace area. Also applied automatically when `maxLength` is above 200. |
| `labels_from` | property name (string) | array of numbers (vector) or matrix | none | Names a sibling array property whose items label this value. A matrix uses it for rows and columns; a vector for its boxes. The two are kept the same length. See [labels_from](#labels_from). |
| `column_labels_from` | property name | matrix | none | Labels the columns from a sibling array; rows stay free (the user can add and remove them). Keeps the number of columns in sync. |
| `row_label` | string | matrix | `"Row"` | Name used for rows when there are no row labels: "Period 1", "Period 2", and "Add period". |
| `symmetric` | boolean | matrix | `false` | Editing or pasting cell (r, c) also sets (c, r). The lower triangle is dimmed. Checked by the form: a matrix that is not equal to its transpose is reported ("Must be symmetric: A / B is 0.5 but B / A is 0.4.") and Run is blocked. A "Make symmetric" link under the grid copies the upper triangle onto the lower one. Not checked by the server. |
| `square` | boolean | matrix | `false` | Hides the add-row and remove-row controls so rows cannot be added or removed alone, and the form requires rows equal to columns ("Must be square: 3 rows, but row 2 has 4 columns."). Not checked by the server. |
| `columns` | list of column objects | `widget: "table"` | none | Table columns, see [table](#table). |
| `columns[].title` | string, required | each column | none | Column header (also used as a key; keep titles unique). |
| `columns[].type` | `"string"` or `"number"` | each column | text | `number` gives a number box; anything else a text box. With `options_from` the type is ignored (a drop-down). |
| `columns[].options_from` | property name | each column | none | A sibling array property; its values are the drop-down choices for that column. A value not in the list is still shown. |
| `columns[].optional` | boolean | each column | `false` | Marks "(optional)" in the header. Empty trailing optional cells are removed from the row (`[a, b]`, not `[a, b, null]`). New rows start without trailing optional columns; an optional column that comes before a required one is filled with `0` or empty text so positions stay right. Still, put optional columns last. |
| `columns[].placeholder` | string | each column | none | Placeholder in that column's boxes (not for drop-downs). |
| `preview` | `"graph"` | `widget: "table"` | none | Draws a live graph beside the table. Nodes are the values of the first column's `options_from` list; the first two columns are the endpoints and a numeric third column is the line weight. Needs at least 2 nodes. Shown only when there is room (wide layout). |
| `max_from` | property name | integer or number | none | The maximum becomes the smaller of `maximum` and the length of the named sibling array; the form shows it and rejects a larger value. See [max_from](#max_from). |
| `slider` | `[min, max, step]` | number or integer | none | Adds a slider with those three numbers next to the number box. |
| `percent` | boolean | number | `false` | Box shows and accepts percentages; stored value is the fraction. |
| `keys_from` | property name | object map | none | Keys of the sibling object property become fixed labelled number boxes. See [map](#additionalproperties-map-keys_from). |
| `key_title` | string | object map | `"Name"` | Label of the key box of each entry and base of the add button ("Add system" for `"System name"`). With `keys_from` and `value_title` it is the first half of the column heading. |
| `key_placeholder` | string | object map | none | Editable list: placeholder in the key box. With `keys_from`: placeholder in each number box (default: the value schema's `default`, else its first `examples` entry, else none). |
| `value_title` | string | object map | none | Title of the value column: heading above a `keys_from` grid ("System → Weight"), and the label of the value editor for map values that are not objects. |
| `add_label` | string | `widget: "table"` | `"Add row"` | Text of the button that adds a row, for example `"Add connection"`. |
| `option_labels` | object of value to label | string enum, array of string enums | none | Display names for the enum values. |
| `placeholder` | string | string | none | Placeholder for text box or text area. |
| `advanced` | boolean | any property | `false` | Moves the field into a collapsed "More options" section at the bottom of its object. See [advanced](#advanced). |
| `also` | list of property names | inside an `anyOf` entry | none | Extra, optional fields shown with that alternative. See [alternatives](#anyof-alternatives-modes). |

Output-only keys `unit` and `format` are not read by the input form (they are documented under output schemas).

What goes wrong when misused:

| Mistake | Result |
|---|---|
| `labels_from` or `options_from` names a property that is missing or not an array | Treated as no labels or no options. A matrix without labels shows "Add names first" if it is empty. |
| `widget: "matrix"` or `"vector"` on something whose declared cell type is not numeric | Ignored. |
| `widget: "table"` without `columns` | Ignored; the array is drawn by its shape. |
| `slider` with the wrong number of entries | Undefined behaviour (the slider uses whatever is in positions 0, 1, 2). Unverified beyond that. |
| `max_from` naming a property that is not an array | Ignored (no cap shown or checked). |
| An unknown `x-ui` key | Ignored without warning. Misspellings are invisible, so check the form in the preview. |

## Defaults

The form starts with each field's schema `default` written into the input, at any depth (objects inside objects, items of cards, map values), and sends it with the run. A user sees the default as a normal value in the box, switch, slider or drop-down.

| Situation | What happens |
|---|---|
| Form opens | The first example is loaded, then every property it lacks that has a `default` gets that default. With no example the form starts from the defaults alone. |
| Required field with no `default` | Starts empty (and reports "This is required." after the first Run). A required object without a default starts as an object holding its children's defaults, if any. |
| Optional object with no `default` | Absent, shown as a dashed "Set ..." button. Its children's defaults apply only after it is set. |
| Optional object with a `default` | Starts set to a copy of that default. |
| "Start from" an example, "Blank" | Same rule: the example (or nothing) plus defaults for what is missing. Blank is therefore the defaults, not `{}`. |
| `anyOf` alternatives | Only the defaults of the active alternative's fields are written; the other alternatives' fields stay out so `additionalProperties: false` keeps passing. |
| Switching alternative, adding a card or a map entry, "Set ..." | New values start from the field's `default`, else its required fields' defaults, else the first enum value, an empty text, `0` for numbers or zeros sized to the label list. |
| User clears a field | The key is removed; the number box then shows the default as a grey placeholder. Your code's default still applies. |
| Server | The API and the SDK use `jsonschema`, which does not fill defaults: they receive exactly what the form sent. |

A default is copied, never shared, so editing a list or object does not change the schema. Keep repeating each `default` in your code (`input.get("risk_aversion", 0.5)`), because API and SDK callers do not go through the form.

## Worked examples

### matrix

From `portfolio-optimization` (covariance, symmetric, labelled by `assets`):

```json
"covariance": {
  "title": "Covariance matrix",
  "description": "Annualised. Paste it from a spreadsheet; it is kept symmetric",
  "type": "array", "items": {"type": "array", "items": {"type": "number"}},
  "x-ui": {"widget": "matrix", "labels_from": "assets", "symmetric": true}
}
```

The user sees: a scrollable grid with sticky headers. The row and column headers are the asset names. Typing in a cell above the diagonal also fills the mirrored cell, and the lower triangle is dimmed. A footer says "Paste straight from Excel or Sheets into any cell". An empty cell is stored as `0`. If an example or a loaded run is not symmetric, the form reports the first differing pair and the footer offers "Make symmetric (copy the upper triangle)". With `type: "integer"` cells the boxes take whole numbers (typed and pasted values are rounded). Enter moves to the cell below, Shift+Enter to the one above, Tab to the next cell.

Paste rules (matrix and vectors): the first cell where you paste is the top-left corner of the pasted block. Separators are detected in this order: tab, comma, semicolon, whitespace. A first row that has no numbers is treated as a header and dropped; a first column that has no numbers is treated as row labels and dropped. A trailing `%` divides by 100. Cells that are not numbers are skipped. Anything beyond the right edge is ignored; rows below the bottom are ignored unless rows can grow. Decimal commas are not supported.

Three row/column modes, decided by the keys:

| Keys | Rows | Columns | Row add/remove |
|---|---|---|---|
| `labels_from: "assets"` | one per label | one per label | no (follow the labels) |
| `column_labels_from: "assets"` | free | one per label | yes |
| neither | free | the length of the first row | yes (unless `square`) |

`returns_history` in the same workflow is the second kind:

```json
"returns_history": {
  "title": "Historical returns",
  "description": "One row per period, one column per asset. Paste from a spreadsheet",
  "type": "array", "minItems": 3, "items": {"type": "array", "items": {"type": "number"}},
  "x-ui": {"widget": "matrix", "column_labels_from": "assets", "row_label": "Period"}
}
```

The user sees columns headed by the asset names, rows named "Period 1", "Period 2", ..., a cross at the end of each row (disabled at 3 rows, the `minItems`) and an "Add period" link.

If the label list is empty the grid is replaced by "Add names first; the grid follows them." The generic QUBO example (`generic-qubo`) uses `labels_from: "labels"` with `square: true`, where `labels` is optional: a blank form shows the message until the user adds variable names, while a loaded example with a full `Q` and no labels shows the grid with numbered rows.

### vector

From `portfolio-optimization`:

```json
"expected_returns": {
  "title": "Expected annual returns",
  "description": "As fractions: 0.12 means 12%",
  "type": "array", "items": {"type": "number"},
  "x-ui": {"widget": "vector", "labels_from": "assets"}
}
```

The user sees one small number box per asset, captioned with the asset name, in a wrapping grid. Pasting several values into a box fills that box and the following ones. Empty boxes are stored as `0`. `integer` items give whole-number boxes. Without `labels_from` and without the widget, a number list is a chip box, or for `minItems == maxItems <= 4`, a fixed row labelled x, y, z, w. With `widget: "vector"` and no `labels_from` you get numbered boxes ("1", "2", ...) with "Add value" and "Remove last" links (hidden when `minItems` equals `maxItems`, and bounded by `minItems`/`maxItems`).

### table

From `max-cut` (edges between named nodes, with a live graph):

```json
"edges": {
  "title": "Connections",
  "type": "array", "minItems": 1,
  "items": {"type": "array", "minItems": 2, "maxItems": 3, "items": {"type": ["string", "number"]}},
  "x-ui": {
    "widget": "table",
    "columns": [
      {"title": "From", "type": "string", "options_from": "nodes"},
      {"title": "To", "type": "string", "options_from": "nodes"},
      {"title": "Weight", "type": "number", "optional": true, "placeholder": "1"}
    ],
    "preview": "graph",
    "add_label": "Add connection"
  }
}
```

The user sees a table with From and To drop-downs (filled from the node chips) and an optional Weight box, a cross per row and an add link ("Add row" by default; the `add_label` key below changes it to "Add connection"). New rows start with the first node in both drop-downs. Enter in a text or number cell moves to the same column of the next row. Leaving Weight empty stores `["a", "b"]`; entering 2 stores `["a", "b", 2]`. Beside it (wide screens) a ring of node circles with a line per connection; line thickness follows the weight.

The row schema is not used to draw the table: only `columns` is. Keep `items` consistent with the columns, since the server validates against `items`.

### dataset

From `quantum-kernel-classifier`:

```json
"definitions": {
  "table": {
    "type": "object", "additionalProperties": false, "required": ["X", "y"],
    "x-ui": {"widget": "dataset"},
    "properties": {
      "X": {"type": "array", "minItems": 4, "maxItems": 400, "items": {"type": "array", "minItems": 1, "maxItems": 8, "items": {"type": "number"}}},
      "y": {"type": "array", "items": {"type": "integer", "minimum": 0, "maximum": 1}}
    }
  }
},
"properties": {
  "train": {"$ref": "#/definitions/table", "title": "Training set", "description": "Feature columns and a 0/1 label"}
}
```

The user sees an empty text area ("Paste rows from a spreadsheet. Features first, a 0/1 label last"), a "Use these rows" link and an "Upload CSV" button (`.csv`, `.tsv`, `.txt`). Pasting parses immediately; typed text needs "Use these rows". After a successful parse it shows a summary ("40 rows, 4 features, labels 18 x 0 and 22 x 1"), a preview of the first 6 rows and a "Replace data" link. The value stored is `{"X": [[...]], "y": [...]}`.

The editor takes its limits from the schema: rows from `X.minItems` / `X.maxItems`, features per row from `X.items.minItems` / `X.items.maxItems`, feature values from `X.items.items.minimum` / `maximum`, and the label from `y.items` (`minimum`, `maximum`, `enum`; `integer` means whole numbers). Where the schema says nothing the old numbers apply: at least 4 rows, 1 to 8 features, label `0` or `1`. The last column is always the label, a header row is skipped if its first row contains any non-number, every row must have the same width, and a message names the offending row. In the example above that means 4 to 400 rows, 1 to 8 features and a label of 0 or 1. Keep `X`/`y` as the property names: the editor is chosen for any object with both, even without `widget`.

### code

From `interaction-energy` / `electrolyte-solvent-screen` (atom coordinates as text):

```json
"xyz": {
  "title": "Geometry",
  "description": "One atom per line: symbol x y z, in Ångström",
  "type": "string", "maxLength": 4000,
  "x-ui": {"widget": "code", "placeholder": "O  0.000  0.000  0.117\nH  0.000  0.757 -0.469\nH  0.000 -0.757 -0.469"}
}
```

The user sees a monospace text area (4 to 10 rows, grows with the text) with the placeholder as a greyed sample. The stored value is the raw string including line breaks. Parsing and checking its content is your workflow's job (raise `WorkflowError`). `maxLength` above 200 gives the same area even without the widget.

### slider

From `portfolio-optimization`:

```json
"risk_aversion": {
  "title": "Risk aversion", "type": "number", "minimum": 0, "default": 0.5,
  "x-ui": {"slider": [0, 2, 0.05]}
}
```

The user sees a range slider from 0 to 2 in steps of 0.05, a small number box next to it, and the current value in the header. The slider starts at `default` (here 0.5), which is already in the input. The slider itself does not enforce `minimum`/`maximum`: typing 5 in a field with `maximum` 2 turns the box red and blocks Run (and the thumb clamps to the end). Make `slider` and the schema bounds agree. For an `integer` field use step 1.

### percent

From `quantum-kernel-classifier` (slider and percent together):

```json
"test_fraction": {
  "title": "Held out for testing", "type": "number", "minimum": 0.1, "maximum": 0.85, "default": 0.33,
  "x-ui": {"slider": [0.1, 0.85, 0.01], "percent": true}
}
```

The user sees a slider whose header reads `33%`, and a box showing `33` with a `%` sign. Typing `40` stores `0.4`. Slider numbers and schema bounds stay as fractions. `percent` can be used without `slider`, giving just the percent box. The header rounds to a whole percent, so use steps of 0.01 or coarser. This is the input key `percent` (a boolean); the output schema's `x-ui.format: "percent"` is a different key and is not read by the input form.

### max_from

From `portfolio-optimization`:

```json
"num_to_select": {"title": "Assets to pick", "type": "integer", "minimum": 1, "x-ui": {"max_from": "assets"}}
```

The user sees "1 to N" next to the box (with only `minimum` 1 and no `maximum`, N is the number of assets entered), and the form rejects a larger value with "Must be at most N, the number of assets you listed." and blocks Run. Only the browser form knows this rule: the server and the SDK accept a larger value, so also check it in `__init__` and raise `WorkflowError`.

### labels_from

Two or more fields stay the same size when the label list changes. In `portfolio-optimization`, `assets` labels `expected_returns` (vector) and `covariance` (matrix); `returns_history` follows with its columns.

- Removing the third asset removes the third entry of the vector, the third row and third column of the matrix, and the third column of the history.
- Adding an asset appends a `0` entry, row and column.
- Any other edit (renaming, reordering by retyping) does not insert or delete: values are only trimmed or padded with `0` at the end.

Rename means the same position; removal is detected only when exactly one item disappears and all the others stayed in order. A matrix cannot grow rows or columns on its own; it follows its labels.

### advanced

From `generic-qubo`:

```json
"offset": {"title": "Constant offset", "type": "number", "default": 0, "x-ui": {"advanced": true}}
```

The user sees the field hidden behind a "More options" link at the bottom of the object it belongs to; clicking expands every advanced field there. It works at any nesting level, hides only the display (the field is still valid, validated and kept), and a required field can be advanced too. Errors inside a collapsed section are not visible: do not mark a `required` field advanced unless the example input already fills it. `advanced` on a field that belongs to an `anyOf` mode is ignored; the mode box wins.

### anyOf alternatives (modes)

From `quantum-kernel-classifier`:

```json
"anyOf": [
  {"title": "Sample dataset", "required": ["dataset"], "x-ui": {"also": ["test_fraction"]}},
  {"title": "My data", "required": ["data"], "x-ui": {"also": ["test_fraction"]}},
  {"title": "My train and test sets", "required": ["train"], "x-ui": {"also": ["test"]}}
]
```

The user sees a box headed "Describe it with" and a row of buttons titled by each entry. Below it only the fields of the selected alternative: its `required` fields plus its `also` fields. The box sits where the first alternative field appears in your property order; fields that belong to no alternative are drawn normally.

| Detail | Behaviour |
|---|---|
| Title | The entry's `title`. Without one: the titles of its required fields joined by "and", or "Option N". |
| Which is selected at start | The first alternative whose required fields are all present in the loaded input; else the first with any of them present; else the first. |
| Choosing | A manual choice sticks until another alternative becomes fully filled (for example by loading an example) while the chosen one is not. |
| Switching away | The fields of the old alternative that are not in the new one are removed from the input, but remembered so switching back restores them. Fields outside any alternative stay. This keeps the input valid with `additionalProperties: false`. |
| Switching in | Each required field of the new alternative gets its remembered value, an existing value, or a starting value (`default`, first enum value, empty object, label-sized zeros). |
| Fallback | If any alternative has no `required` and no `also` fields, or the object has no `properties`, the whole `anyOf` is ignored by the form: all fields are drawn flat. The validator still applies it. |
| Server | `anyOf` is an ordinary schema rule: any one satisfied entry passes. A user who fills two alternatives still passes. The error for none is "Fill in the fields for the option you chose." |

Alternatives work on every object the form draws, including the values of a map (see below), not only the top level. Use `anyOf` of `required` lists only; entries with their own `properties`, or `oneOf`, are not turned into tabs.

### additionalProperties map (keys_from, key_title, key_placeholder, option_labels)

An object with no `properties` and a schema in `additionalProperties` is a map from names to values. Two forms.

Editable list of entries, from `interaction-energy`:

```json
"systems": {
  "title": "Systems", "type": "object", "minProperties": 1, "maxProperties": 6,
  "x-ui": {"key_title": "System name", "key_placeholder": "complex"},
  "additionalProperties": {
    "type": "object", "additionalProperties": false,
    "anyOf": [
      {"title": "Known molecule", "required": ["name"]},
      {"title": "XYZ coordinates", "required": ["xyz"]}
    ],
    "properties": { "name": {"title": "Molecule", "type": "string"}, "xyz": {"type": "string", "x-ui": {"widget": "code"}} }
  }
}
```

The user sees one card per entry. The card header is a text box for the key (spaces become `_`; renaming to an empty or existing name is rejected; Enter or leaving the box commits), a remove cross, and the value's own fields below. "Add system" (from `key_title`, with a trailing " name" removed and lower-cased; `"Name"` gives "Add name") adds an entry named `system_1`, `system_2` and so on. `maxProperties` hides the Add button at the limit. If the value schema is not an object (numbers, text, a switch, a list) each entry shows one editor for the value, labelled with `value_title` (default "Value"); a new number entry starts at `0` (or the `minimum`), text at an empty string.

Fixed grid of numbers keyed by another property, from `interaction-energy`:

```json
"terms": {
  "title": "How to combine the energies", "type": "object", "additionalProperties": {"type": "number"},
  "x-ui": {"keys_from": "systems", "key_title": "System", "value_title": "Weight"}
}
```

The user sees a heading "System → Weight" (from `key_title` and `value_title`, shown when `value_title` is set) and one number box per key found in `systems`, captioned with the key. A placeholder in each box comes from `key_placeholder`, else the value schema's `default`, else its first `examples` entry, else none. If `systems` is empty: "Add systems first." (using the title of the keys property). Emptying every box removes the whole property. Values are drawn as number boxes (whole numbers when the value schema is `integer`), whatever else the value schema says.

Enum labels, from `electrolyte-solvent-screen`:

```json
"solvents": {
  "type": "array", "uniqueItems": true, "minItems": 1, "maxItems": 7,
  "items": {"type": "string", "enum": ["EC", "FEC", "DMC", "DME", "THF", "ACN", "water"]},
  "x-ui": {"option_labels": {"EC": "Ethylene carbonate (EC)", "water": "Water"}}
}
```

The user sees toggle buttons (stored values are `EC`, `water`, ...) in the enum's order and cannot pick more than `maxItems`. Unlabelled values show their raw text. For a single string enum use `option_labels` the same way (6-31g* shows as "6-31G*").

### Objects, optional sections and lists of objects

From `electrolyte-solvent-screen`: an optional object that starts hidden and advanced.

```json
"active_space": {
  "type": "object", "additionalProperties": false, "required": ["n_electrons", "n_orbitals"],
  "default": {"n_electrons": 4, "n_orbitals": 4},
  "x-ui": {"advanced": true},
  "properties": {
    "n_electrons": {"title": "Electrons", "type": "integer", "minimum": 2, "maximum": 8, "multipleOf": 2},
    "n_orbitals": {"title": "Orbitals", "type": "integer", "minimum": 2, "maximum": 6}
  }
}
```

The user sees, under "More options", a dashed "Set active space" button. Pressing it fills the object from its `default` and shows the two boxes in a panel with a "Use default" link that removes the object again (so your code's default applies). `multipleOf` is not hinted; give the rule in the `description`.

`custom_solvents` in the same schema is an array of objects: each item is a card with its fields (two code areas and text boxes), a remove cross and an "Add" link.

## Validation: form vs server

| Check | In the browser form | On the server | In the SDK / lint |
|---|---|---|---|
| Valid JSON object | JSON view blocks Run until it parses | `422` | `qsolve validate` |
| Types, `required`, `enum`, `minimum`, `maximum`, `minItems`, `maxItems`, `minLength`, `maxLength`, `minProperties`, `maxProperties`, `additionalProperties`, `anyOf`, `uniqueItems`, `multipleOf`, `exclusiveMin/Max` | Ajv (draft-07, all errors, non-strict); Run is blocked and the message shown | `jsonschema`, the schema's draft (`$schema`); first best-matching error returned as `422 invalid_input` | same library before `__init__` (`Invalid input at <path>: <message>`) and on examples (`E023`) |
| `format` | not checked | not checked | not checked |
| `pattern`, `patternProperties`, `format: "regex"`, non-local `$ref` | n/a | n/a | `E021` at lint |
| Schema is valid draft-07 | if it fails to compile the form skips client checks | n/a | `E020` |
| Schema size above 256 KB | n/a | n/a | `E022` |
| Request body above the limit | n/a | `max_input_bytes`, 1,000,000 bytes by default (verified in the platform settings) | n/a |
| `x-ui` | never | never | never |
| `max_from`, `symmetric`, `square` | checked by the form (inline message, Run blocked) | not enforced | not enforced |
| Dataset editor limits (rows, features, label from `X`/`y`) | entry rules in the editor; the schema's own `minItems`/`maxItems`/`minimum`/`maximum` are checked by Ajv as usual | schema keywords only | schema keywords only |
| Solver-setting ranges (`min`/`max` of a parameter) | checked in the run composer before submit | checked by the API | n/a |
| Cross-field rules ("node names unique", "num_to_select at most len(assets)", "matrix is symmetric") | no | no | your `__init__` raises `WorkflowError("...")` |

Messages in the form (from the form's validator wrapper):

| Keyword | Message |
|---|---|
| `required` | This is required. |
| `type` | Must be a whole number. / Must be a number. / Must be a `<type>`. |
| `minimum`, `maximum` | Must be at least N. / Must be at most N. |
| `minItems`, `minProperties` | Add at least N. |
| `maxItems`, `maxProperties` | At most N allowed. |
| `minLength` | This can't be empty. (`minLength` 1) / Use at least N characters. (N above 1) |
| `x-ui.max_from` | Must be at most N, the number of `<titles>` you listed. |
| `x-ui.symmetric` | Must be symmetric: `<cell>` is a but `<mirror cell>` is b. |
| `x-ui.square` | Must be square: R rows, but row K has C columns. |
| `maxLength` | Keep this under N characters. |
| `enum` | Choose one of the options. |
| `additionalProperties` | "x" isn't a field this workflow accepts. |
| `anyOf` | Fill in the fields for the option you chose. |
| anything else | the validator's own message, capitalised |

Server and SDK messages use the Python validator's wording (for example `[] is too short`). To make a custom rule readable for users, put it in the `description` or raise `WorkflowError` in `__init__`; both run only after the schema passes.

If `$schema` is missing the lint accepts it as draft-07 but the server and SDK pick the validator from `$schema`, falling back to the library's newest draft (unverified in practice). Always write `"$schema": "http://json-schema.org/draft-07/schema#"`.

## Pitfalls

Do:

- Give every property a `title`, and a `description` with units and the rule the schema cannot express.
- Set `additionalProperties: false` on every object you control.
- Use a string `enum` (with `option_labels`) for choices.
- Put a complete example first in `inputs.examples`; every example must validate (`E023`).
- Use `items: {"type": "string", "enum": [...]}` for "pick several".
- Match `slider` ranges and `minimum`/`maximum`; match `maxItems` to the manifest `envelope`.
- Put label lists before the fields they label, and make them `required` or part of the same alternative; the labelled grid shows "Add names first" until they exist.
- Put optional table columns last.
- Re-check the form on a local platform preview or the workflow run page after any schema change, because `x-ui` mistakes are silent.
- Check cross-field rules in `__init__`.

Do not:

- Rely on `x-ui` to restrict anything. It is presentation only.
- Rely on `max_from`, `symmetric` or `square` for correctness outside the browser form: API and SDK callers skip them. Re-check in `__init__`.
- Use `pattern`/`patternProperties`/`format: "regex"`, or a `$ref` that is not `#/...` (lint errors `E021`).
- Name a property `pattern` or `patternProperties`: lint scans all keys and reports `E021` even for a property of that name (the check does not distinguish keywords from property names). Rename the field, for example `motif`. (Observed in the lint source; not run.)
- Use a property with only `oneOf`/`anyOf`, or a `type` list that mixes other types. The form draws the first non-null type or a raw JSON box.
- Use tuple-form `items: [a, b]` (JSON box).
- Use `enum` of numbers expecting a drop-down.
- Count on `default` for API or SDK callers: only the form fills defaults (see [Defaults](#defaults)).
- Put commas in values that users will enter as chips; they split on commas.
- Rely on `x-order` written by hand; it is overwritten on publish.

## Checklist before publishing

1. `$schema` is `http://json-schema.org/draft-07/schema#`; top level is `"type": "object"`.
2. `qsolve validate .` and `qsolve lint .` pass: no `E020` to `E024`.
3. No `pattern`, `patternProperties`, `format: "regex"`, or remote `$ref`; no property named `pattern`.
4. Schema is below 256 KB; each example input and any pasted data stays below the request limit (1,000,000 bytes by default).
5. Every property has a `title`; those with units, formats or hidden rules have a `description`.
6. Every object has `additionalProperties: false` (unless it is a deliberate map).
7. Bounds (`minItems`, `maxItems`, `minimum`, `maximum`, `maxLength`) are set and agree with the manifest `envelope`.
8. Every `x-ui` key was checked against this page's table for the right shape and spelled exactly; every `labels_from`, `column_labels_from`, `options_from`, `max_from`, `keys_from` names a real sibling property of the right kind.
9. `matrix`, `vector`, `table`, `dataset` and `code` fields were opened in the form and tried with a spreadsheet paste.
10. Every `default` is repeated in the workflow code; `max_from`, symmetry and other cross-field rules are also checked in `__init__`.
11. If the workflow uses alternatives, each `anyOf` entry has a `title` and `required` fields, and `additionalProperties: false` is set so switching alternatives never leaves stale fields.
12. The first example input is complete and loads cleanly; blank form and each alternative were tried.
13. `advanced` is used only on fields that are optional or already filled.
