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.
Updated 13 November 2026 View as markdown
On this page
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. For the input side, see 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. |
| Does the schema decide the order of the answer? | Only through x-order, if you write one (see 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:
- The quantum method and the reference run. If either produced no value, the run fails.
answer()is called on the quantum outcome. Any exception becomesanswer() failed on the quantum result: <ExceptionType>: <message>.- The answer is validated against the output schema. A mismatch fails the run with:
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.
- The answer becomes the
answerfield 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, " |
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). |
| 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. |
| object whose values are all objects | A table, one row per key, first column the key. See Tables. |
| any other object | A titled box holding the object's own figures, chips and tables. See 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. |
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-orderif 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
titleof that field in the row schema, else the key in words, plus(unit)if it hasx-ui.unit. Headers areth scope="col"; for a record of records the key of each row is ath scope="row". - Cells apply
format,precisionand 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 isadditionalProperties, used when it is a schema (nottrue/false). - For a record of records the first column holds the record keys in words, for example
h2becomes "H2". Column schema lookup is through the row schema'sproperties.
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. |
| 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.
Worked examples
Units on a scalar (molecular-ground-state)
"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)
"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)
"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)
"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)
"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)
"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)
"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, andrequiredlists every field thatanswer()always returns. - Every field that may be missing is declared
["type", "null"](oranyOfwith{"type": "null"}), andanswer()returnsNonethere, not a string such as"n/a". - Every property you want labelled has a
titlewritten as a person would say it, and adescriptionwhere 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 schemapropertiesfor a table). -
format: percentis used only on fractions (0.143 for 14.3%). Percentages already multiplied by 100 useunit: "%". - Bulk data is in
Outcome.detailsor in its own top-level key, not inside the answer's main fields. - If field order matters,
x-orderlists it (see Order). - Key names still read acceptably as words for any key you leave without a
title. -
qsolve lint .passes, andqsolve runon an example finishes withoutWorkflow produced an invalid answer.
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