# Parameters

> How to declare parameters in qsolve.yaml: every field, the five types and their exact checks, ranges and choices, how defaults and overrides are validated, how the run form shows them and how they reach your code.

Source: https://qsolve.omlabs.co/docs/build/parameters
Updated: 2026-11-13

Parameters are settings a user can change without changing the problem: shots, circuit depth, an iteration count, a mode switch. Declare each under `parameters` in `qsolve.yaml`; Qsolve checks every value before your code runs. Parameters are separate from the problem input, which is described by the [input schema](/docs/build/input-and-output-schemas).

## Declaring a parameter

```yaml
parameters:
  - {name: layers, label: Circuit depth, type: int, default: 3, min: 1, max: 8, description: "More layers can find better answers but take longer to train"}
  - {name: shots, label: Final shots, type: int, default: 4000, min: 100, max: 20000, description: Final sampling shots}
  - {name: train_on, label: Train angles on, type: enum, default: sim, choices: [sim, backend], description: "sim trains noise-free and uses the device only to measure"}
  - {name: penalty_scale, label: Penalty strength, type: float, default: 1.0, min: 0.1, max: 20, description: Cardinality penalty multiplier}
```

The first three come from `max-cut`, the last from `portfolio-optimization`. Every key is one of these seven, and any other key in an entry fails the manifest load:

| Key | Type | Required | Default | Meaning and constraints |
|---|---|---|---|---|
| `name` | string | Yes | | The key your code reads from `self.params`. Unique within the manifest (`parameter names must be unique`). The model puts no restriction on the characters, so use a Python identifier (`optimizer_iters`) to keep it usable as a key and in `-p name=value`. |
| `type` | string | Yes | | Exactly `int`, `float`, `bool`, `str` or `enum`. |
| `default` | any | Yes | | Used when the user gives no value. It is checked with the same rules as a user's value, so a default that fails them makes the manifest invalid with that rule's message. There is no way to declare "no default". |
| `label` | string | No | `""` | The name shown in the form, at most 60 characters. When empty, the form shows the name with `_` and `-` replaced by spaces and the first letter capitalised (`optimizer_iters` shows as "Optimizer iters"). |
| `description` | string | No | `""` | What changing it does. No length limit in the model. Shown as an info tooltip next to the label. |
| `min` | number | No | none | Inclusive lower bound. Read only for `int` and `float`; ignored for other types. |
| `max` | number | No | none | Inclusive upper bound. Same rules as `min`. |
| `choices` | list | For `enum` | none | The allowed values. Read only for `enum`; ignored for other types. |

Checked when the manifest loads (a failure makes the manifest invalid with the message shown): `min` greater than `max` (`parameter 'x': min 5.0 is greater than max 1.0`), an `enum` with no or an empty `choices` list (`parameter 'x': an enum needs a non-empty `choices` list`), and a `min` or `max` that is NaN or infinite. A `default` is checked like any value. The choices of an `enum` are not required to be unique.

## The five types

| Type | Accepts | Rejects | Error message (`'x'` is the parameter name) |
|---|---|---|---|
| `int` | whole numbers, including negative | booleans, numbers with a fractional part or written as `3.0`, strings | `parameter 'x' must be an int` |
| `float` | any finite number, integers included | booleans, NaN, infinity, strings | `parameter 'x' must be a number` (NaN and infinity: `must be a finite number`) |
| `bool` | `true` or `false` | anything else, including `0` and `1` | `parameter 'x' must be a bool` |
| `str` | text, any length, including empty | anything else | `parameter 'x' must be a string` |
| `enum` | a value equal to one of `choices` (a bool never equals the int `1`: `true` is not the choice `1`) | any other value | `parameter 'x' must be one of ['sim', 'backend']` |

For `int` and `float` the bounds are checked after the type, and a value outside them is rejected with `parameter 'shots' must be >= 100.0` or `parameter 'shots' must be <= 20000.0`. Bounds are stored as numbers, so the message shows a decimal point even for whole bounds. Bounds never apply to `bool`, `str` or `enum`. NaN and infinity never pass for `int` or `float`, so a NaN cannot slip through a bound.

Values are not converted. A `float` parameter keeps an integer as an integer (`default: 1` reaches your code as `1`, not `1.0`), so convert in your code when it matters: `float(self.params["penalty_scale"])`. A `str` parameter has no length limit or pattern. If you need a closed set of strings, use `enum`.

## How values reach your code

Qsolve resolves the parameters before it builds your class: it starts from the defaults, overlays the user's overrides and checks every value. The result is a dictionary holding every declared parameter, which is passed to `__init__` and stored as `self.params`:

```python
p, shots = int(self.params["layers"]), int(self.params["shots"])
```

- **Every declared name is present.** You can index `self.params` without checking for missing keys.
- **Every value has passed the checks above.** Do not re-validate type and range.
- **Undeclared names never arrive.** An override naming an undeclared parameter is rejected with `unknown parameters: ['name']` (the names are sorted).
- **The resolved set is recorded.** It is stored with the run, shown on the run page and kept in the receipt (`provenance.params`), so a run records the values it really used, defaults included.
- **Parameters are also passed to `estimate()`** through `self.params`, because `estimate()` is a method of the same instance.

The seed is not a parameter. It is a separate field of every run and reaches your code as `ctx.seed`.

## Where overrides come from

| Place | How | When a value is wrong |
|---|---|---|
| Local run | `qsolve run . -i input.json -p layers=1 -p train_on=backend`. The text after `=` is parsed as JSON first, so `500` is a number, `true` is a bool, `0.5` is a float and `fast` (not valid JSON) stays text. | `Error:` plus the message, exit code 1. A `-p` without `=` is a usage error: `--param expects key=value`. |
| Benchmark case | `params:` in `benchmarks/cases.yaml`. Missing names take the default. See [Benchmarks](/docs/build/benchmarks). | The case fails with the message. |
| Run form | Every declared parameter gets a control (see below). | The API answers `422 invalid_params` with the message. |
| API | The `params` object of `POST /v1/runs`; names omitted take the default. | `422 invalid_params`, message as above. |

A string value such as `"500"` is a `str`, not an `int`. Over the API and in YAML, write numbers as numbers.

## How the run form shows them

The run page collects the parameters in a collapsible section called "Solver settings", closed by default and showing "Tuned defaults" until something differs from the default, then "N changed". The controls follow the type:

| Type | Control | Notes |
|---|---|---|
| `enum` | A row of buttons, one per choice | Shows each choice as text. |
| `bool` | A switch | |
| `int`, `float` | A number box; for `int` a typed value is rounded to a whole number | When both `min` and `max` are set, the range is printed beside it ("100 to 20,000"); outside the range the box is marked invalid. |
| `str` | A text box | |

Every control is labelled with `label` (or the generated name) and carries `description` as a tooltip. A "Reset to defaults" button appears once any value is changed. The form always submits all parameters, defaults included, in declared order of the manifest.

Two behaviours to design around:

- **Give both bounds, or the range is not shown.** With only a `min` or only a `max`, the box is still marked invalid when you leave the bound, but no range text appears.
- **The form does not stop an out-of-range number by itself.** The input form's own validity check covers the problem input; the parameter values are checked on the server, which refuses the run with `invalid_params` and the message above. Put bounds in the manifest and rely on that check.
- **Use string choices for an `enum`.** The form sends the clicked choice as text, so an enum declared with numeric choices such as `[1, 2]` would be submitted as `"1"` and refused with `must be one of [1, 2]`. Use `[one, two]`-style text, and convert in your code.

Parameter controls have no `x-ui` hints: those apply to the problem input only. They are described in the [input schema reference](/docs/build/input-schema-reference).

## Choosing parameters well

Expose the settings that change cost, accuracy or run time, and give each a bound that keeps a run inside your `resources.timeout_s` and at a sensible cost. A `shots` parameter with no `max` lets a user ask for a run you did not plan for, and the platform runs at most 100,000 shots per circuit (`qsolve.MAX_SHOTS_PER_CIRCUIT`): lint `E025` is an error when a `shots` parameter has no `max`, a `max` above that or a `default` above it.

- **Name the shots parameter `shots`.** When a workflow gives no `estimate()`, the default estimate uses `params["shots"]` (and `0` if there is none) as its total shots, with one circuit, and is marked as a default; see [Writing the workflow](/docs/build/writing-the-workflow).
- **Use an `enum` for modes, not a string.** `constraint_mode: [mixer, penalty]` is checked, a free string is not.
- **Make the default the setting you would recommend.** Researchers run defaults. In `max-cut` the defaults are 3 layers, 4000 final shots and noise-free training (`train_on: sim`).
- **If changing a setting changes which problem is solved, it is an input, not a parameter.**
- **Reflect cost-driving parameters in your `estimate()`.** A run that sends many more circuits than estimated fails the `estimate_honest` gate. In the examples, `train_on: backend` sends one circuit per optimiser step to the target, and the estimate counts them, which is why `train_shots` and `optimizer_iters` are bounded (`max: 8000` and `max: 400` in `max-cut`).
- **Bound by the limits you advertise.** `quantum-kernel-classifier` uses `min: 0` with the documented meaning "0 = auto-tune" for `reps` and `bandwidth`; a sentinel like this is a normal int or float, so say what it means in `description`.
- **Defaults must be cheap enough to validate.** The platform runs your benchmark cases with their `params`, falling back to the defaults, inside the validation time limit.

See [Writing the workflow](/docs/build/writing-the-workflow) for `estimate()` and [The manifest](/docs/build/manifest) for the rest of `qsolve.yaml`. Every lint code is in [Lint codes](/docs/build/lint-codes).
