SDK: runner and manifest
Reference for qsolve.runner, qsolve.manifest and qsolve.comparison: run_workflow, estimate_run, validate_input, RunOutput, load_workflow, Manifest and its parts, run_methods, headline and implementation.
Updated 13 November 2026 View as markdown
On this page
The harness that runs a workflow, the objects that describe one on disk, and the comparison that turns method outputs into numbers. The platform calls the same functions inside its sandbox. Back to the SDK reference.
qsolve.runner
from qsolve.runner import RunOutput, estimate_run, run_workflow, validate_input
__all__ of the module: RunOutput, WorkflowError, estimate_run, run_workflow, validate_input. estimate_run and run_workflow are also exported from qsolve.
run_workflow
run_workflow(
workflow_dir: str,
input: dict[str, Any],
*,
target: str = "sim-cpu",
params: dict[str, Any] | None = None,
seed: int = 0,
backend: Backend | None = None,
commit_sha: str | None = None,
image_digest: str | None = None,
on_event: Sink | None = None,
) -> RunOutput
Runs every method the manifest declares on one input, measures everything and builds the result. This is what qsolve run and the platform call. Who calls it: the CLI, the sandbox entry point, run_benchmarks, and your own tests.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
workflow_dir |
str |
required | Directory holding qsolve.yaml (a Path also works at runtime; the annotation is str). |
input |
dict |
required | The input, validated against the input schema. |
target |
str |
"sim-cpu" |
sim-cpu, sim-gpu, fake:<device>[@scale] or ibm:<device>. Ignored when backend is given. See Backends and the Qiskit bridge. |
params |
dict | None |
None |
Parameter overrides by name. Unknown names or out-of-range values raise. Declared parameters you do not override use their defaults. |
seed |
int |
0 |
The run's seed, available as ctx.seed. The same input, parameters and seed give the same result. |
backend |
Backend | None |
None |
Use this backend object instead of resolving target. For tests and for the platform. |
commit_sha |
str | None |
None |
Recorded in provenance.commit_sha. |
image_digest |
str | None |
None |
Recorded in provenance.image_digest. |
on_event |
Sink | None |
None |
A function receiving each progress event dict. |
Returns a RunOutput.
Raises WorkflowError for: invalid parameters (unknown parameters: [...], parameter 'x' must be <= 6.0), input that does not match the schema (Invalid input at <path>: <message>), an unusable target or backend (unknown target, no QSOLVE_IBM_TOKEN, missing ibm extra, unknown fake device, noise scale outside 1 to 10), a class that cannot be imported or is not a Workflow subclass, a constructor failure, an input above the envelope, a quantum or reference method that produced no value (a reference whose answer() fails or breaks the output schema counts as no value), a failing answer(), or an answer that breaks the output schema (Workflow produced an invalid answer: ...). Also raises FileNotFoundError, ValueError or pydantic.ValidationError from load_workflow when the manifest or schemas are invalid (the CLI turns those into a message for validate, test, run and estimate). Exceptions that are not WorkflowError raised inside your quantum or reference method propagate, with a note raised in the quantum method '<id>'.
Side effects. Imports your workflow package (and, to keep two workflows with the same package name src apart, removes stale modules of that name from sys.modules and puts the workflow root first on sys.path). Limits BLAS to one thread for the duration of the run so summation order is fixed and the same seed gives bit-identical results on your laptop and in the sandbox. Sends circuits to the backend (on ibm: targets that spends your own quota). Emits events: {"type": "stage", "stage": "build"}, then {"type": "stage", "stage": "run", "device": ..., "target": ...}, then {"type": "stage", "stage": "compare"}, plus whatever your methods emit.
What it computes.
| Output | Rule |
|---|---|
result.objective |
the quantum method's recomputed value |
result.baseline_objective, baseline_solver |
the reference's value and its manifest title |
result.gap |
(quantum - reference) / max(abs(reference), 1e-12); negative when the quantum result is better |
result.uncertainty |
kind="sampling" with stderr and the note one standard deviation on the quantum value when the quantum method gave a stderr, otherwise kind="exact" |
result.summary |
workflow.summary(...) or the default sentence, cut to 500 characters |
provenance.input_hash, result_hash |
SHA-256 of the canonical JSON, floats rounded to a 1e-6 grid |
provenance.target |
the backend's target class (sim-cpu, sim-gpu or qpu), not the string you passed |
from qsolve import run_workflow
out = run_workflow(".", {"Q": [[-1, 2], [2, -1]]}, params={"shots": 500}, seed=1,
on_event=lambda e: print(e["type"], e.get("stage")))
print(out.result.metrics.matches, out.provenance.result_hash)
Gotchas. target is not checked until after parameters and input validate, so a bad target with a bad input reports the input error first. The first failure wins; nothing is partial. Do not call it from inside a workflow.
estimate_run
estimate_run(
workflow_dir: str,
input: dict[str, Any],
*,
target: str = "sim-cpu",
params: dict[str, Any] | None = None,
) -> Estimate
Validates and sizes a run without executing any method. Used for price quotes and credit holds, and by qsolve estimate. It loads the manifest, resolves parameters, validates the input, builds your workflow class (so __init__ and size run, and the envelope is enforced) and calls estimate(target).
| Behaviour | Detail |
|---|---|
Workflow returns None |
An Estimate(qubits=size(), circuits_on_target=1, shots_on_target=params["shots"] or 0, notes=["the workflow gives no estimate; using a conservative default"], by_default=True). |
Workflow returns an Estimate |
by_default is forced to False, and if runtime_s is set the fixed run overhead RUN_OVERHEAD_S (20 s) is added and the sum rounded to 0.1 s. |
| Errors | WorkflowError as for run_workflow. No circuit is run and no backend is created, so the target string is passed to your estimate unchecked. |
There is no seed or on_event parameter. The CLI exposes no --param, so qsolve estimate uses the manifest defaults; call the function directly to estimate with overrides.
validate_input
validate_input(wf: LoadedWorkflow, input: dict[str, Any]) -> None
Checks input against wf.input_schema with jsonschema.validate. Returns None when valid. Raises WorkflowError("Invalid input at <dotted.path>: <message>"), where the path is <root> for a top-level problem. Called by run_workflow, estimate_run, qsolve validate (for the examples) and run_benchmarks. If the schema itself is invalid, jsonschema.SchemaError propagates; qsolve lint reports that as E020.
RunOutput
class RunOutput(BaseModel):
result: Result
provenance: Provenance
What run_workflow returns. out.model_dump(mode="json") is exactly what qsolve run --out writes and what the sandbox sends as its result frame. Fields are described in Results and events.
Other module-level names
| Name | Value or role |
|---|---|
HASH_DECIMALS |
6: floats are rounded to an absolute 1e-6 grid before hashing. Finer would make the reproducibility gate flaky. |
SOLVER_DETAILS_MAX_BYTES |
200_000: larger Outcome.details are replaced by {"omitted": "<n> bytes, over the 200000 kept"}. |
WorkflowError |
Re-export of qsolve.workflow.WorkflowError. |
Private helpers (underscore names such as _instantiate, _context, _hash, _stable, _plain, _source, _make_importable) are not part of the API.
qsolve.manifest
from qsolve.manifest import Manifest, load_workflow, LoadedWorkflow
The parsed form of qsolve.yaml. Every model rejects unknown keys (extra="forbid"). The meaning of each manifest field, with examples, is in The manifest and Parameters; this section is the Python surface.
load_workflow
load_workflow(root: str | Path) -> LoadedWorkflow
Reads qsolve.yaml in root, validates it and reads both JSON Schema files. Returns a LoadedWorkflow. Raises:
| Exception | When |
|---|---|
FileNotFoundError("No qsolve.yaml in <root>") |
there is no qsolve.yaml |
ValueError("qsolve.yaml is not valid YAML: ...") |
YAML error |
pydantic.ValidationError (a ValueError) |
the manifest breaks a rule |
ValueError("schema file not found: <rel>"), "<rel> must contain a JSON object", "schema path escapes workflow directory: <rel>" |
a schema path is missing, not an object, or points outside the workflow directory |
json.JSONDecodeError (a ValueError) |
a schema file is not valid JSON |
Side effect: reads files only. It does not import your code or validate the schemas as JSON Schema.
LoadedWorkflow
| Field | Type | Meaning |
|---|---|---|
root |
Path |
The resolved workflow directory. |
manifest |
Manifest |
The parsed manifest. |
input_schema |
dict |
The input JSON Schema. |
output_schema |
dict |
The output JSON Schema. |
Manifest
| Field | Type | Default | Notes |
|---|---|---|---|
schema_version |
Literal[1] |
required | Must be 1. The module constant SCHEMA_VERSION is 1. |
name |
str |
required | Matches ^[a-z0-9][a-z0-9-]{1,62}$. |
version |
str |
required | Semantic version MAJOR.MINOR.PATCH. |
family, summary, license |
str |
required | |
tags |
list[str] |
[] |
|
inputs |
SchemaRef |
required | schema: path (Python name schema_path) and examples: list. |
parameters |
list[Parameter] |
[] |
Names must be unique, each default must pass its own check. |
outputs |
SchemaRef |
required | |
entrypoint |
str |
required | package.module:ClassName, matching ^[A-Za-z_][\w.]*:[A-Za-z_]\w*$. |
category |
str |
"" |
At most 60 characters. |
objective |
ObjectiveDecl |
label="Objective", unit="", decimals=4 |
decimals is 0 to 10. |
metric |
MetricDecl |
kind="relative_gap" |
kind is relative_gap, relative_error or error_rate; note up to 600 characters; tolerance 0 to 1 (default 0); tolerance_abs optional, at least 0. |
methods |
list[MethodDecl] |
required, at least 2 | Ids unique; exactly one quantum and exactly one reference. |
envelope |
Envelope |
required | max_qubits 1 to 1000, max_variables 1 to 100000, notes string. |
targets |
Targets |
simulators=["cpu"], qpu=[], min_qubits_qpu=None |
simulators items cpu or gpu; qpu items ibm (Braket is not available yet). min_qubits_qpu is accepted but ignored by the platform (lint W052). |
resources |
Resources |
timeout_s=1800, memory_mb=4096, network="none" |
timeout_s 1 to 86400; memory_mb 256 to 65536; network can only be none. |
runtime |
Runtime |
python="3.12", dependencies="requirements.lock" |
Sub-models: SchemaRef(schema_path, examples), Parameter(name, type, default, label, description, min, max, choices), ObjectiveDecl, MetricDecl, MethodDecl(id, role, title<=80, what<=500, strength<=300, limit<=300), Envelope, Targets, Resources, Runtime. MethodDecl.id follows the name rule (lowercase letters, digits, dashes, 2 to 63 characters).
Manifest.resolve_params
resolve_params(self, overrides: dict[str, Any] | None = None) -> dict[str, Any]
Returns every declared parameter: the override if given, otherwise the default, each passed through Parameter.check. Raises ValueError for unknown parameters: [...] and for any value of the wrong type or outside its bounds. Called by run_workflow and estimate_run, which convert the ValueError to WorkflowError.
Parameter.check
check(self, value: Any) -> Any
Returns value unchanged or raises ValueError. Rules: int rejects bools and non-ints; float accepts finite ints and floats but not bools, NaN or infinity; bool needs a real bool; str needs a string; enum needs a value in choices (a bool never matches the int 1); min and max apply to int and float. A Parameter itself is rejected at load when min > max, when an enum has no choices, or when min or max is NaN or infinite. Messages look like parameter 'layers' must be <= 6.0 (bounds are stored as floats, so an integer bound prints with .0).
qsolve.comparison
from qsolve.comparison import headline, implementation, run_methods
Runs the declared methods and computes the numbers. run_workflow calls it; you do not need to.
implementation
implementation(wf: Workflow, decl: MethodDecl) -> Any
The bound method of wf that implements decl: quantum for role quantum, reference for role reference, classical_<id with - replaced by _> for role classical. Raises WorkflowError("method 'x' is declared in the manifest but the workflow has no name method") when it is missing or not callable.
run_methods
run_methods(
wf: Workflow,
decls: list[MethodDecl],
*,
quantum_ctx: Context,
classical_ctx: Context,
recorder: Recorder,
) -> tuple[list[MethodResult], dict[str, Outcome]]
Runs every declared method, reference and classical ones first and the quantum method last, each with its own context, and returns a MethodResult per declaration (in manifest order) and the Outcome of each method that produced one (by id). For every method it sets recorder.method, times it, and:
- a
MethodUnavailablemakes it unavailable with your message; - a classical method that raises anything else is unavailable with
failed: <Type>: <msg>; a quantum or reference method that raisesWorkflowErrorre-raises it, and any other exception re-raises with a note; - a non-quantum method that sent circuits is unavailable (
it sent circuits to the quantum backend, which only the quantum method may do); - a non-finite objective or error bar makes it unavailable;
- otherwise
evaluaterecomputes the value (verified="recomputed") or, with no solution, the reported value is kept (verified="reported"); a difference between claimed and recomputed values larger than1e-6 * max(1, |value|)is stored inclaimed; answer(outcome)is attempted. For a reference or classical method, a failinganswer()or an answer that breaks the output schema (passed asoutput_schema, whichrun_workflowsupplies) makes that method unavailable with the reason (answer() failed: ...orits answer breaks the output schema: ...); the run goes on. For the quantum method a failure leavesanswer=Nonehere and the runner reports it (WorkflowError).
After the loop it fills vs_reference = (value - ref) / max(|ref|, 1e-12) for each method, and for sampling methods p_reference, valid_fraction and top_outcomes (the five most probable, each labelled by its label or its JSON, cut at 60 characters). p_reference counts samples whose recomputed value is at most the reference value (with a 1e-9 relative slack).
headline
headline(decl: MetricDecl, methods: list[MethodResult], quantum: Outcome) -> Metrics
The accuracy against the reference with one of three generic formulas, scale = max(abs(reference), 1e-12):
kind |
accuracy |
accuracy_pm |
matches when |
|---|---|---|---|
relative_gap |
1 - max((q - ref) / scale, 0) |
none | accuracy is within tolerance of 1; or abs(q - ref) <= tolerance_abs; or the quantum stderr is at most a quarter of scale and abs(q - ref) <= 2 * stderr; or q <= ref |
relative_error |
1 - abs(q - ref) / scale |
stderr / scale |
the same three conditions as above, without the q <= ref rule |
error_rate |
1 - q |
half-width of the 95% Wilson interval for a rate q measured on quantum.details["n_test"] cases (none if n_test is missing or below 1) |
q <= ref + tolerance + tolerance_abs, or the 95% interval's lower end is at most ref + tolerance + tolerance_abs |
accuracy is clipped to 0 to 1. Raises ValueError if the quantum or reference method has no value. See Benchmarks for how a workflow author chooses a kind.