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.
Updated 13 November 2026 View as markdown
On this page
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.
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-uionly changes how a field is drawn. The server and the SDK ignore it. In the browser form a fewx-uirules (max_from,symmetric,square) are also checked before Run, as listed under Validation.- The form starts every field at its schema
defaultand sends it (see Defaults). Besides the schema, the form itself checksmax_from,symmetric,squareand 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:
{
"$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:
- Put
"additionalProperties": falseon every object so a misspelled field is an error, not silently ignored. - Give every property a
titleand adescription. The title is the label; the description appears in a small info popover next to the label. - Declare bounds (
minItems,maxItems,minimum,maximum,maxLength) so mistakes are caught before a run is created. - Add at least one example input (
inputs.examplesin 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), 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) 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). |
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. |
x-ui.percent |
yes | Shows and accepts percent, see percent. |
x-ui.max_from |
yes | Shown as a hint and checked by the form. See 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 of fixed-length rows |
| 2 | array of arrays of number or integer (or of untyped items with x-ui.widget: "matrix") |
Matrix grid; 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: 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 |
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 |
no properties, additionalProperties is a schema |
A 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 |
additionalProperties: true or omitted, and no properties |
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. |
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. |
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. |
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. |
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. |
also |
list of property names | inside an anyOf entry |
none | Extra, optional fields shown with that alternative. See alternatives. |
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):
"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:
"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:
"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):
"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:
"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):
"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:
"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):
"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:
"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
0entry, row and column. - Any other edit (renaming, reordering by retyping) does not insert or delete: values are only trimmed or padded with
0at 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:
"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:
"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:
"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:
"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:
"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.
"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 adescriptionwith units and the rule the schema cannot express. - Set
additionalProperties: falseon every object you control. - Use a string
enum(withoption_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
sliderranges andminimum/maximum; matchmaxItemsto the manifestenvelope. - Put label lists before the fields they label, and make them
requiredor 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-uimistakes are silent. - Check cross-field rules in
__init__.
Do not:
- Rely on
x-uito restrict anything. It is presentation only. - Rely on
max_from,symmetricorsquarefor correctness outside the browser form: API and SDK callers skip them. Re-check in__init__. - Use
pattern/patternProperties/format: "regex", or a$refthat is not#/...(lint errorsE021). - Name a property
patternorpatternProperties: lint scans all keys and reportsE021even for a property of that name (the check does not distinguish keywords from property names). Rename the field, for examplemotif. (Observed in the lint source; not run.) - Use a property with only
oneOf/anyOf, or atypelist 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
enumof numbers expecting a drop-down. - Count on
defaultfor API or SDK callers: only the form fills defaults (see Defaults). - Put commas in values that users will enter as chips; they split on commas.
- Rely on
x-orderwritten by hand; it is overwritten on publish.
Checklist before publishing
$schemaishttp://json-schema.org/draft-07/schema#; top level is"type": "object".qsolve validate .andqsolve lint .pass: noE020toE024.- No
pattern,patternProperties,format: "regex", or remote$ref; no property namedpattern. - Schema is below 256 KB; each example input and any pasted data stays below the request limit (1,000,000 bytes by default).
- Every property has a
title; those with units, formats or hidden rules have adescription. - Every object has
additionalProperties: false(unless it is a deliberate map). - Bounds (
minItems,maxItems,minimum,maximum,maxLength) are set and agree with the manifestenvelope. - Every
x-uikey was checked against this page's table for the right shape and spelled exactly; everylabels_from,column_labels_from,options_from,max_from,keys_fromnames a real sibling property of the right kind. matrix,vector,table,datasetandcodefields were opened in the form and tried with a spreadsheet paste.- Every
defaultis repeated in the workflow code;max_from, symmetry and other cross-field rules are also checked in__init__. - If the workflow uses alternatives, each
anyOfentry has atitleandrequiredfields, andadditionalProperties: falseis set so switching alternatives never leaves stale fields. - The first example input is complete and loads cleanly; blank form and each alternative were tried.
advancedis used only on fields that are optional or already filled.