# Output schema reference

> Everything about schemas/output.json: how the answer is validated, how each JSON shape is drawn for users, every x-ui hint the answer view reads, what happens on misuse, with examples from published workflows.

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

`schemas/output.json` is a JSON Schema (draft-07) for the dictionary your `answer()` returns. It does two jobs: Qsolve validates the quantum answer against it, and the answer view uses it for labels, help text, units, number formats and precision. This page lists every key the code reads, every shape and how it is drawn, and what goes wrong when you misuse them. For the rules shared with the input schema (draft-07, no `pattern`, local `$ref` only, 256 KB), see [Input and output schemas](/docs/build/input-and-output-schemas). For the input side, see [Input schema reference](/docs/build/input-schema-reference).

## What the schema does, and does not do

| Question | Answer |
|---|---|
| Is the quantum answer validated against it? | Yes, always, by `jsonschema.validate` after `answer()` returns. |
| Are the other methods' answers validated? | No. They are shown as returned; if their `answer()` raises, that method simply has no answer. |
| Does the schema decide what the headline accuracy is? | No. The headline comes from the manifest's `metric` and is computed by the platform. Nothing in `output.json` can change it. See [Benchmarks](/docs/build/benchmarks). |
| Does the schema decide the order of the answer? | Only through `x-order`, if you write one (see [Order](#order)). |
| Does a wrong `x-ui` hint reject an answer? | No. Validation ignores `x-ui`. A misspelled or unsupported hint is silently dropped. |
| Are the other methods' answers shown? | Yes, on the run page, under the answer, with the same schema. |

## Validation

The run does these steps in order:

1. The quantum method and the reference run. If either produced no value, the run fails.
2. `answer()` is called on the quantum outcome. Any exception becomes `answer() failed on the quantum result: <ExceptionType>: <message>`.
3. The answer is validated against the output schema. A mismatch fails the run with:

```text
Workflow produced an invalid answer: <first validation message>
```

The message is the first error the validator finds, for example `'energy_hartree' is a required property` or `'abc' is not of type 'number'`. It does not name the path, so keep your schema specific and test with `qsolve run`.

4. The answer becomes the `answer` field of the result. It must be a dictionary: the result model requires an object, so declare `"type": "object"` at the top.

Method answers (for the non-quantum methods) are produced by `answer()` too, inside a try block. If it raises, that method's answer is `None` and no answer is offered for it. They are never checked against the schema, so an answer with the wrong shape for a classical method is still displayed, drawn from its shape with whatever hints match.

Allow `null` where a value can be missing. Declaring `"type": ["number", "null"]` lets `answer()` return `None`; a plain `"type": "number"` fails the run on `None`. The answer view draws a null as `–`.

## Where the answer is shown

| Place | Schema used? | Notes |
|---|---|---|
| Run page, "Answer" card | Yes | The run's own version's output schema is delivered with the run. Titles, help text, units, formats and precision apply. |
| Run page, "<method>: its answer" under the Answer card | Yes | One per non-quantum method that returned an answer (runs recorded before methods were stored have none). Same schema, but not validated against it (see above). |
| Workflow page, benchmark detail, "The quantum answer" | Yes | Entries over 20,000 characters are left out (see [Large values](#large-values)). |
| Workflow page, benchmark detail, "Show its answer" on a method card | Yes | One per method that has an answer. |

All three use one renderer, so a hint written once behaves the same everywhere. A run is drawn with the schema of the version it ran, not the latest one.

The platform also shows the run's `summary` (your optional `summary()` method, or a default sentence), the quantum-versus-classical comparison, and the quality card, none of which come from `output.json`.

## How shapes are drawn

The answer view knows nothing about any workflow. It looks at the value's shape. The label of a field is its `title` if the schema has one at that place, otherwise the key turned into words (`num_to_select` becomes "Num to select"; underscores and dashes become spaces, a camel-case boundary gets a space, the first letter is capitalised, keys of two characters or fewer are left as written).

| JSON value | What the user sees |
|---|---|
| number, string, boolean, `null` at an object level | A labelled figure with a copy button (copies the value as returned). All of an object's scalars are gathered in a grid at the top of its section. |
| string longer than 28 characters | The same figure in smaller text instead of large numerals. Long text wraps. |
| array of scalars | Chips, one per item, in order. Empty array: the word "none". |
| array of objects | A table, one row per object. See [Tables](#tables). |
| object whose values are all objects | A table, one row per key, first column the key. See [Tables](#tables). |
| any other object | A titled box holding the object's own figures, chips and tables. See [Nested objects](#nested-objects). |
| array of arrays, or mixed array | Chips whose text is the item as JSON, for example `["a","b"]`. No schema hints are applied. |
| any value over 20,000 characters | A preview with "Show all" and "Download". See [Large values](#large-values). |

Everything that is not a scalar (arrays and objects) is drawn after the figures, each under a small heading with its label.

### Scalars

| Value | Display |
|---|---|
| `true` / `false` | `Yes` / `No` |
| `null` or missing | `–` |
| integer (a JSON number with no fractional part) | Thousands separators, English style: `12345` shows `12,345`. |
| other number | Six significant digits (`0.1234567` shows `0.123457`). Magnitudes of 100,000 or more, or under 0.001, use exponent form with six significant digits (`1.23457e+5`, `5.00000e-4`). Zero shows `0`. |
| string | As written. |

Because Python's `3.0` is the JSON number `3.0`, which the browser reads as an integer, a float with no fractional part is drawn as an integer.

### `x-ui.unit`

A string appended after a numeric value, after a non-breaking space (so the unit never wraps onto its own line): `-1.137 Ha`, `12 kcal/mol`. It applies to numbers only; on a string or boolean it does nothing. The unit is plain text and is not interpreted, so `Ha` and `mHa` are different strings and no conversion happens. Put the hint on the property that holds the number, on the array or object of numbers, or on `items`.

In a table column, the unit goes in the column header (`Quantum energy (Ha)`) and is removed from the cells.

### `x-ui.format`

| Value | Display |
|---|---|
| `"percent"` | The number is a fraction: `0.143` shows `14.3%`. Any size is shown as it is: `1.43` shows `143.0%`, `2.5` shows `250.0%`. `unit` is ignored. |
| `"integer"` | Rounded to a whole number with thousands separators: `1234.6` shows `1,235`. `unit` still applies. |
| `"scientific"` | Exponent form with 4 significant digits: `0.00001236` shows `1.236e-5`; zero shows `0`. `unit` still applies. |
| anything else | Ignored. |

Percent details: one decimal by default (`0.5` shows `50.0%`, `-0.05` shows `-5.0%`); from 99% up to just under 100%, two decimals, cut rather than rounded, so `0.99997` shows `99.99%` and a value below 1 never reads `100%` by rounding. Exactly `1` shows `100.0%`. On non-numbers the format is ignored.

### `x-ui.precision`

A whole number from 0 to 12, ignored if it is anything else. For `percent` it is the number of decimals (`0.14349` with precision 2 shows `14.35%`). For plain numbers it is the number of significant digits (default 6): `3.14159265` with precision 3 shows `3.14`. For `scientific` it is the significant digits (default 4). It does not apply to `integer`. A whole number with a precision set is drawn with that many significant digits, not with separators.

If your number is already a percentage (for example 87.2), do not use `format`; use `"x-ui": {"unit": "%"}` as `interaction-energy` does for `correlation_recovered_pct`.

### Lists of scalars

Each item is drawn as a chip. `unit`, `format` and `precision` written on `items`, or on the array property itself, apply to every chip (the hint on `items` wins). `quantum-kernel-classifier` shows both: its `test_accuracy_ci95` array puts `x-ui: {format: percent}` on `items`, so the two chips read `78.3%` and `92.1%`.

### Tables

A list of records, or a record of records, becomes a table:

- Columns are every field found in the rows, in the order of the row schema's `x-order` if present, then the order first seen. None are dropped; a wide table scrolls sideways (keyboard users can focus it and use the arrow keys).
- A field whose value is an array of scalars is shown as comma-separated text, and any other array or object as short JSON (cut at 80 characters, full text on hover).
- A row missing a field shows `–` in that cell.
- Column header: the `title` of that field in the row schema, else the key in words, plus `(unit)` if it has `x-ui.unit`. Headers are `th scope="col"`; for a record of records the key of each row is a `th scope="row"`.
- Cells apply `format`, `precision` and the number rules above. The unit is not repeated in the cells.
- For a list, the row schema is `items`. For a record of records, it is `additionalProperties`, used when it is a schema (not `true`/`false`).
- For a record of records the first column holds the record keys in words, for example `h2` becomes "H2". Column schema lookup is through the row schema's `properties`.

### Nested objects

An object that is not a table is drawn as a shaded box with its own figures, lists and tables, and its fields get titles from the object's `properties`. The answer itself is level 0. Objects at levels 1 and 2 are open boxes. Deeper objects (level 3 and below) sit behind a "Show N fields" control, collapsed until opened. An object nested deeper than 6 levels is shown as JSON behind "Show all". A record of records is a table at any depth.

### Help text, references and nullable values

| Keyword | Effect on display |
|---|---|
| `description` | Help text behind an (i) next to the label of a figure or section. Not shown on table column headers. |
| `$ref` to `#/definitions/...` or `#/$defs/...` | Followed. Keywords next to the `$ref` win over the referenced ones; `x-ui` is merged. Remote and missing references are ignored. |
| `anyOf` / `oneOf` | A `{"type": "null"}` branch is dropped, so a nullable number keeps its title, unit and format. With several non-null branches their `properties` are merged and the first branch's other keywords are used. |
| `allOf` | Merged in. |
| `type`, `enum`, `minimum`, `maximum`, `required`, `default`, `if`/`then` | Validation only. The drawing follows the actual value. |
| `x-ui` keys other than `unit`, `format`, `precision` | The input form's hints (`widget`, `slider`, `columns` and so on) have no effect on the answer. |

A hint on an array or on an object whose values are numbers (`weights`) applies to every number inside it that has no hint of its own. In a table, hints come from the row schema's `properties` only.

### Keys that are data

Per-key titles for a record whose keys are data (asset names) are not possible: those labels are the keys in words. For per-asset titles or units, return a list of records, for example `[{"asset": "AAPL", "weight": 0.25}]`, and declare `weight` under `items.properties`.

### Order

Fields appear in the order of `x-order` if the schema has one at that level (a list of key names; keys it does not name follow in the order the answer had), then in the order of the stored answer. Scalars are grouped first, then lists and objects. The schema's `properties` order is not used: the database stores objects with sorted keys, and the platform records `x-order` for the input schema only. A hand-written `x-order` in `output.json` is stored as written. Without it, an answer's own key order is not guaranteed.

### Large values

On the run page, any value whose JSON text is longer than **20,000 characters** (a string, an array, an object, or a list with more than 10,000 items) is not drawn in full. The user sees its size, the first 600 characters, "Show all" (up to 200,000 characters, scrollable) and "Download" (the whole value as a file). Nothing inside it is formatted. A value under the limit is drawn normally.

When a benchmark result is stored for the detail view, the answer is compacted separately: every top-level entry whose JSON text is longer than 20,000 characters (Python's default separators) is dropped from "The quantum answer", and a dropped entry leaves no heading. Nothing nested is trimmed.

Put long per-point data (predictions per test point, full histograms) under its own top-level key so only that entry is affected. For `quantum-kernel-classifier`, `predictions` and `baseline_predictions` are such keys. Keep the answer small, and put bulk data in `Outcome.details`, which is kept with the receipt.

## What the platform stores and shows beyond the answer

None of these come from `output.json`; they are measured by the SDK harness and the platform. Do not put them in your answer to appear to supply them.

| Item | Source | Shown as |
|---|---|---|
| Headline accuracy and error bar | The manifest's `metric` kind, computed from the method values | Large percentage in benchmark detail, with the metric `note` below it. `matches` produces the verdict "Matches the best classical answer" or "Below the best classical answer". |
| Each method's `value`, `stderr`, `vs_reference`, `seconds` | Outcomes of each method | "Every method, on this input" table, with the objective's label, unit and decimals from the manifest's `objective`. |
| `verified` per method | `recomputed`: the method returned a `solution` and the SDK re-evaluated it with `evaluate()`. `reported`: no solution to re-evaluate, so it is the workflow's own figure. | "Re-evaluated by Qsolve" or "Reported by the workflow". A re-evaluated value that differs from what the method claimed is recorded (`claimed`) and fails validation. |
| Mitigation: `fidelity_raw`, `fidelity_mitigated`, `fidelity_floor`, `postselection_rate`, `methods`, `source`, `coverage` | Measured by the SDK from recorded circuits and counts. The mitigated figure comes from what the workflow reports through `ctx.report_mitigation`, recomputed against the ideal output; with no report it equals the raw figure. | Fidelity tiles and track ("measured" or "reported"), shots kept, gain, step list, source (real hardware, device noise model, ideal simulator). See [Error mitigation and fidelity](/docs/build/error-mitigation-and-fidelity). |
| Circuit depth, two-qubit gates, qubits, circuits, shots | Recorded by the SDK | Quality card |
| `summary` | Your `summary()` method, else `"<quantum title>: <value>; <reference title>: <value> (accuracy <x%>)."`, cut to 500 characters | Sentence above the answer |
| `Outcome.details` | Your method | "How the solver got there"; kept with the receipt if under 200,000 bytes, otherwise replaced with a note that it was omitted. |

The headline metric `error_rate` needs `n_test` in the quantum method's `Outcome.details`; see [The manifest](/docs/build/manifest#metric).

## Worked examples

### Units on a scalar (`molecular-ground-state`)

```json
"energy_hartree": {"type": "number", "title": "Quantum energy", "x-ui": {"unit": "Ha"}},
"error_vs_exact_mha": {"type": "number", "title": "Error vs exact", "x-ui": {"unit": "mHa"}},
"within_chemical_accuracy": {"type": "boolean", "title": "Within chemical accuracy"}
```

In the benchmark detail the user sees three figures in a grid, for example "Quantum energy" `-1.13727 Ha` and "Error vs exact" `0.4 mHa` (illustrative values: six significant digits, then the unit string), and "Within chemical accuracy" `Yes`.

### Nullable value (`molecular-ground-state`)

```json
"ccsd_t_full_space_hartree": {"type": ["number", "null"], "title": "CCSD(T), full space", "x-ui": {"unit": "Ha"}}
```

When the classical CCSD(T) check was not computed (above 60 basis functions), `answer()` returns `None` and the user sees "CCSD(T), full space" with `–`.

### A record of records as a table (`molecular-ground-state`, `interaction-energy`)

```json
"systems": {
  "type": "object", "title": "Systems computed",
  "additionalProperties": {
    "properties": {
      "energy_hartree": {"title": "Quantum energy", "x-ui": {"unit": "Ha"}},
      "stderr_hartree": {"title": "Error bar", "x-ui": {"unit": "Ha"}},
      "qubits": {"title": "Qubits"}
    }
  }
}
```

The user sees a table headed "Systems computed", one row per system key, columns "Quantum energy (Ha)", "Error bar (Ha)", "Qubits". The row schema sits under `additionalProperties`; the nested `properties` provide the titles and units.

### A list of records as a ranked table (`electrolyte-solvent-screen`)

`ranking` is an array of objects with 11 fields. The schema's `items.properties` give each column a title and, for energies, `unit: "kcal/mol"`. All 11 columns are shown and the table scrolls sideways on a narrow screen; put the fields you most want seen first (or list them in `x-order` on `items`). Nullable columns such as `noise_free_kcal_mol` and `mp2_full_space_kcal_mol` show `–` when null.

### Percent figures (`portfolio-optimization`, `quantum-kernel-classifier`)

```json
"expected_return": {"type": "number", "title": "Expected return", "x-ui": {"format": "percent"}},
"test_accuracy": {"type": "number", "minimum": 0, "maximum": 1, "title": "Test accuracy", "x-ui": {"format": "percent"}}
```

`0.143` shows `14.3%`; `0.995` shows `99.5%` (99.50% at the default precision); `1.43` shows `143.0%`. The constraints `minimum` and `maximum` guard validation and have no visual effect.

### Lists of words as chips (`max-cut`)

```json
"group_a": {"type": "array", "items": {"type": "string"}, "title": "Group A"}
```

The user sees "Group A" followed by one chip per node. `cut_weight` and `total_weight` show as numbers in the grid above, titled "Weight of the cut" and "Total edge weight".

### A small object of figures (`quantum-kernel-classifier`)

```json
"confusion_matrix": {
  "type": "object", "title": "Confusion matrix",
  "properties": {"tp": {"title": "True positives"}, "tn": {"title": "True negatives"},
                 "fp": {"title": "False positives"}, "fn": {"title": "False negatives"}}
}
```

The user sees a box headed "Confusion matrix" with four labelled integers. The sub-properties have no `type`, so the schema only titles them and does not constrain them; that is valid, and the display does not need `type`.

### A bulk key (`quantum-kernel-classifier`)
### A bulk key (`quantum-kernel-classifier`)

`predictions` is an array of integers with no title: the heading reads "Predictions". For a large test set its JSON text can pass 20,000 characters (roughly 6,600 one-digit entries with separators). On the run page it then shows as a preview with "Show all" and "Download"; in the benchmark detail the entry is left out while the other fields stay.

### A mapping with data keys (`generic-qubo`, `portfolio-optimization`)

```json
"assignment": {"type": "object", "additionalProperties": {"type": "integer"}, "title": "Assignment (variable to 0 or 1)"},
"weights": {"type": "object", "additionalProperties": {"type": "number"}, "title": "Portfolio weights", "x-ui": {"format": "percent"}}
```

The user sees a box headed with the title and one labelled figure per variable (the variable names in words). Hints on the object, or on `additionalProperties`, apply to every value: the weights `{"AAPL": 0.25, "MSFT": 1.43}` read `25.0%` and `143.0%`. Titles per variable are not possible, because the schema does not list the keys.

## Misuse and what happens

| Mistake | What happens |
|---|---|
| `answer()` returns a key the schema does not list, and the schema allows extras (the default) | Valid. Displayed with its key as a label and no hints. |
| Same, with `"additionalProperties": false` at the top | Run fails: `Workflow produced an invalid answer: Additional properties are not allowed ('x' was unexpected)`. |
| A required key is missing | Run fails: `Workflow produced an invalid answer: 'x' is a required property`. |
| A wrong type (for example `None` for `number`) | Run fails: `Workflow produced an invalid answer: None is not of type 'number'`. |
| `answer()` raises | Run fails: `answer() failed on the quantum result: <ExceptionType>: <message>`. |
| `answer()` returns a non-dict | The schema should reject it (`"type": "object"`); without that, building the result fails. |
| `pattern`, `patternProperties`, remote `$ref`, `format: regex`, over 256 KB | Lint errors `E021` and `E022`; the workflow cannot be published. |
| Hint on a node that cannot use it (`format: percent` on a string, `unit` on a string) | Silently ignored for that value. |
| `format` other than `percent`, `integer`, `scientific` | Silently ignored. |
| `precision` that is not a whole number from 0 to 12 | Silently ignored. |
| `$ref` to something that does not exist, or a remote `$ref` | Ignored: the property is drawn without the referenced hints. |
| Several branches in `anyOf` / `oneOf` | Their `properties` are merged and the first branch's other keywords win; no branch is chosen by the value. |
| Table with many columns | All columns are shown; the table scrolls sideways. |
| Object nested more than 6 levels deep | Shown as JSON behind a "Show all" control. |
| Value over 20,000 characters | Collapsed to a preview with "Show all" and "Download". |
| NaN or infinity in the answer | Not valid JSON. Avoid: return `None` with a nullable type instead. |

## Pre-publish checklist

- [ ] `"type": "object"` at the top, and `required` lists every field that `answer()` always returns.
- [ ] Every field that may be missing is declared `["type", "null"]` (or `anyOf` with `{"type": "null"}`), and `answer()` returns `None` there, not a string such as `"n/a"`.
- [ ] Every property you want labelled has a `title` written as a person would say it, and a `description` where a user may ask what it means.
- [ ] Every physical quantity has `x-ui.unit`, on the property holding the number (or on the array, the object of numbers, or the row schema `properties` for a table).
- [ ] `format: percent` is used only on fractions (0.143 for 14.3%). Percentages already multiplied by 100 use `unit: "%"`.
- [ ] Bulk data is in `Outcome.details` or in its own top-level key, not inside the answer's main fields.
- [ ] If field order matters, `x-order` lists it (see [Order](#order)).
- [ ] Key names still read acceptably as words for any key you leave without a `title`.
- [ ] `qsolve lint .` passes, and `qsolve run` on an example finishes without `Workflow produced an invalid answer`.

```bash
qsolve validate .     # loads both schemas, validates the example inputs
qsolve lint .         # E020 to E023 on the schemas
qsolve test .         # runs the example and the checks end to end
```

## See also

- [Input and output schemas](/docs/build/input-and-output-schemas)
- [Input schema reference](/docs/build/input-schema-reference)
- [Writing the workflow](/docs/build/writing-the-workflow)
- [The manifest](/docs/build/manifest)
- [Benchmarks](/docs/build/benchmarks)
