Documentation
Get started with QObserva in minutes. Complete guides, examples, and API reference.
Quick Start
Get QObserva running in 3 steps. Current release: qobserva 0.1.9 (what's new).
1. Install QObserva
Install QObserva together with the adapter extras for the SDK you use:
pip install qobserva "qobserva-agent[qiskit]"
Replace qiskit with braket, cirq, pennylane, pyquil or dwave, or use all-sdks for all six. The dashboard is prebuilt, so no Node.js is needed. Python 3.12 supports every SDK (see Python compatibility).
Or install from source:
git clone https://github.com/BuildersArk/qobserva.git
cd qobserva
pip install -e packages/qobserva_agent -e packages/qobserva_collector -e packages/qobserva_local -e packages/qobserva
cd packages/qobserva_ui_react && npm install # dashboard dev server (needs Node.js)
2. Start QObserva
qobserva up
This starts the collector (http://127.0.0.1:8080) and the dashboard. Open http://localhost:3000 in your browser. Stop everything with qobserva down from another terminal, or Ctrl+C. If QObserva is already running, qobserva up tells you where instead of starting a second copy.
3. Instrument Your Code
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
from qobserva import observe_run
@observe_run(
project="my_project",
tags={"sdk": "qiskit", "algorithm": "bell_state"},
backend="statevector_sampler", # V2 primitive results don't name their backend
provider="local_sim",
)
def my_quantum_algorithm():
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
return StatevectorSampler().run([qc], shots=1024).result()
my_quantum_algorithm()
Return the SDK's result object from the decorated function. QObserva records the run in the background, so your program never waits for it. If the collector isn't running, you get one warning and your code carries on.
Always include tags={"sdk": "..."} in your decorator for reliable adapter selection. Supported values: "qiskit", "braket", "cirq", "pennylane", "pyquil", "dwave".
Python Version Compatibility
Recommended: Python 3.12 (supports all 6 SDKs). Each SDK's example was run on Python 3.12, 3.13 and 3.14 in September 2026 (QObserva 0.1.6; 0.1.7 and 0.1.8 were re-checked on Python 3.12 with every SDK, 0.1.8 from PyPI):
| SDK | Version tested | 3.12 | 3.13 | 3.14 |
|---|---|---|---|---|
| Qiskit | 2.5.2 | ✅ | ✅ | ✅ |
| Braket | 1.127 | ✅ | ✅ | ✅ |
| Cirq | 1.7.0 | ✅ | ✅ | ✅ |
| PennyLane | 0.45.1 | ✅ | ✅ | ✅ |
| pyQuil | 4.20 / 4.21 | ✅ | ❌ | ❌ |
| D-Wave (dimod) | 0.12.22 | ✅ | ✅ | ✅ |
pyQuil itself supports only Python 3.11–3.12, so qobserva-agent[pyquil] skips it on 3.13 and later. QObserva's own packages support Python 3.10+.
SDK Examples
Each example below is complete: save it as a file, start qobserva up, and run it. The same examples are in the examples folder on GitHub.
Qiskit
from qobserva import observe_run
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
@observe_run(
project="qiskit_test",
tags={"sdk": "qiskit", "algorithm": "bell_state"},
benchmark_params={"target_bitstrings": ["00", "11"]},
backend="statevector_sampler", # V2 primitive results don't name their backend
provider="local_sim",
)
def run():
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
sampler = StatevectorSampler()
return sampler.run([qc], shots=1024).result()
run()
Install: pip install qobserva "qobserva-agent[qiskit]". Backends: Aer runs are labeled local_sim / aer_simulator automatically. For IBM hardware, return the job and set await_result=True, or pass backend=service.backend("ibm_...").
Amazon Braket
from qobserva import observe_run
from braket.circuits import Circuit
from braket.devices import LocalSimulator
@observe_run(
project="braket_test",
tags={"sdk": "braket", "algorithm": "bell_state"},
benchmark_params={"target_bitstrings": ["00", "11"]},
)
def run():
bell = Circuit().h(0).cnot(0, 1)
device = LocalSimulator()
return device.run(bell, shots=1024).result()
run()
Install: pip install qobserva "qobserva-agent[braket]". Works on Python 3.12–3.14. No AWS credentials needed for LocalSimulator.
Cirq
from qobserva import observe_run
import cirq
@observe_run(
project="cirq_test",
tags={"sdk": "cirq", "algorithm": "bell_state"},
measurement_key="result", # Required! Must match cirq.measure(..., key=...)
benchmark_params={"target_bitstrings": ["00", "11"]},
)
def run():
q0, q1 = cirq.LineQubit(0), cirq.LineQubit(1)
circuit = cirq.Circuit()
circuit.append(cirq.H(q0))
circuit.append(cirq.CNOT(q0, q1))
circuit.append(cirq.measure(q0, q1, key="result"))
return cirq.Simulator().run(circuit, repetitions=1024)
run()
Install: pip install qobserva "qobserva-agent[cirq]". Note: the measurement_key parameter is required.
PennyLane
from qobserva import observe_run
import pennylane as qml
@observe_run(
project="pennylane_test",
tags={"sdk": "pennylane", "algorithm": "bell_state"},
benchmark_params={"target_bitstrings": ["00", "11"]},
)
def run():
dev = qml.device("default.qubit", wires=2)
@qml.set_shots(1024)
@qml.qnode(dev)
def circuit():
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
return qml.counts()
return circuit()
run()
Install: pip install qobserva "qobserva-agent[pennylane]". Works with qml.counts() and qml.expval(). qml.set_shots needs PennyLane 0.42+; on older versions pass shots=1024 to qml.device instead.
pyQuil
from qobserva import observe_run
from pyquil import Program, get_qc
from pyquil.gates import H, CNOT, MEASURE
@observe_run(
project="pyquil_test",
tags={"sdk": "pyquil", "algorithm": "bell_state"},
benchmark_params={"target_bitstrings": ["00", "11"]},
backend="2q-qvm",
provider="local_sim",
)
def run():
program = Program()
ro = program.declare("ro", "BIT", 2)
program += H(0)
program += CNOT(0, 1)
program += MEASURE(0, ro[0])
program += MEASURE(1, ro[1])
program.wrap_in_numshots_loop(1024)
qc = get_qc("2q-qvm")
return qc.run(qc.compile(program))
run()
Install: pip install qobserva "qobserva-agent[pyquil]" on Python 3.11–3.12. pyQuil needs Rigetti's QVM and quilc servers, e.g. docker run -d -p 5000:5000 rigetti/qvm -S and docker run -d -p 5555:5555 rigetti/quilc -R.
D-Wave
from qobserva import observe_run
import dimod
@observe_run(
project="dwave_test",
tags={"sdk": "dwave", "algorithm": "qubo"},
backend="ExactSolver", # a SampleSet doesn't say which sampler produced it
provider="local_sim",
)
def run():
Q = {(0, 0): -1, (0, 1): 1, (1, 2): 1}
bqm = dimod.BinaryQuadraticModel.from_qubo(Q)
return dimod.ExactSolver().sample(bqm)
run()
Install: pip install qobserva "qobserva-agent[dwave]". QObserva records the lowest energy. Add "ground_state_energy" to benchmark_params to get an approximation ratio.
API Reference
@observe_run Decorator
@observe_run(
project: str, # Project name (e.g., "my_project")
tags: dict = None, # Tags dict (must include "sdk")
capture_program: str = "full", # "full", "hash", "none" or "attachment"
await_result: bool = False, # Wait for a returned cloud job and record its results
circuit = None, # The circuit that ran, for circuit metrics and text
measurement_key: str = None, # Required for Cirq
benchmark_id: str = None, # Optional benchmark identifier
benchmark_params: dict = None, # Optional benchmark parameters
endpoint: str = None, # Collector URL (default: local collector)
api_key: str = None, # Token if the collector requires one
backend = None, # Backend object or name, if the result doesn't say
provider: str = None, # Provider label, e.g. "ibm", "aws_braket", "local_sim"
)
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
project |
str | Yes | Project name for grouping runs (e.g., "qiskit_test") |
tags |
dict | Yes | Tags dict. Must include "sdk" key. Recommended: "algorithm", "test" |
await_result |
bool | No | If your function returns a cloud job (IBM Runtime job, Braket task, Google Quantum Engine job), QObserva waits for it, records its results, backend and job details, and the decorated function returns the result. Without it, a cloud job is recorded as pending with its job id and returned unchanged. Jobs from local simulators are always read. |
circuit |
circuit | No | The Qiskit, Cirq, Braket or pyQuil circuit that ran. QObserva records its qubits, depth, two-qubit gates and gate counts, plus its OpenQASM (Quil for pyQuil) and a text diagram. Not needed when the job or result already carries the circuit (IBM Runtime jobs, Braket results, Cirq Engine jobs, PennyLane QNodes). |
measurement_key |
str | Cirq only | Measurement key matching cirq.measure() key |
benchmark_id |
str | No | Benchmark identifier for comparison |
benchmark_params |
dict | No | Algorithm-specific values. "target_bitstrings" enables the success-probability metric; "ground_state_energy" enables the approximation ratio. |
backend |
object or str | No | The backend the run executed on, when the SDK result doesn't include it (e.g. AerSimulator(), service.backend("ibm_brisbane"), "statevector_sampler") |
provider |
str | No | Provider label to record, e.g. "ibm", "aws_braket", "local_sim" |
endpoint, api_key |
str | No | Send runs to a different collector, or authenticate with one that requires a token |
capture_program |
str | No | What to record about the circuit: "full" (default since 0.1.8: metrics, a hash, OpenQASM/Quil and a diagram), "hash" (metrics and a hash only, the 0.1.7 behavior), "none" (nothing) or "attachment". Run data goes only to the collector you configure (local by default). |
Provider and backend labels
QObserva records the provider and backend that the SDK's result actually reports. When the result doesn't say, you'll see unknown rather than a guess; pass backend= and provider= to label those runs. Examples: Aer and IBM's fake backends are local_sim, IBM hardware is ibm with the device name, Braket's LocalSimulator is local_sim, AWS-managed simulators are aws_braket (e.g. sv1), Braket QPUs are labeled by maker (e.g. ionq / Forte-1), Cirq's Quantum Virtual Machine is local_sim with the processor name (e.g. willow_pink (virtual)), PennyLane plugins are labeled by the device they ran (e.g. ibm / ibm_fez for qiskit.remote), and the Rigetti QVM is local_sim.
Results computed exactly, without sampling (a statevector Estimator, PennyLane in analytic mode, Braket with shots=0), show Exact instead of a shot count.
Configuration
Everything works with the defaults. These environment variables change them:
| Variable | Default | Description |
|---|---|---|
QOBSERVA_ENDPOINT | local collector | Where your code sends runs (http://127.0.0.1:8080/v1/ingest/run-event) |
QOBSERVA_HOST_MODE | hash | Machine identity on each run: hash, raw (hostname) or none |
QOBSERVA_ASYNC | 1 | Set to 0 to send each run before your function returns |
QOBSERVA_DATA_DIR | user data folder | Where the collector stores its database and run data |
QOBSERVA_COLLECTOR_PORT, QOBSERVA_UI_PORT | 8080, 3000 | Ports used by qobserva up |
QOBSERVA_LOCAL_TOKEN | unset | When set, the collector requires this token (the dashboard passes it automatically) |
In a notebook or test that reads runs back immediately, call qobserva.flush() to wait until pending runs are delivered. The full list is in the API reference.
Privacy and Security
- Everything runs on your machine. Nothing is sent to BuildersArk or any third party.
- Secret values in error messages (API keys, tokens, passwords) are redacted before they're stored.
- The machine's hostname is stored as a hash by default.
- Only pages on your own machine can send data to the collector, so websites you visit can't.
Common Use Cases
Algorithm Tagging
Tag your runs with algorithm names to enable algorithm-specific dashboards:
tags={
"sdk": "qiskit",
"algorithm": "vqe", # Enables algorithm comparison
"test": "variational"
}
Common algorithm tags: "bell_state", "grover", "vqe", "qaoa", "qubo", "qft"
Benchmark Parameters
Include algorithm-specific metrics for better analysis:
@observe_run(
project="vqe_experiment",
tags={"sdk": "qiskit", "algorithm": "vqe"},
benchmark_id="h2_ground_state",
benchmark_params={
"energy": -1.137,
"convergence_iterations": 10
}
)
Viewing Results
After running your instrumented code:
- Open
http://localhost:3000in your browser - Filter by project name (e.g.,
qiskit_test) - View run details, compare runs, and analyze trends
Troubleshooting
- "could not record run telemetry … Is the collector running?" Start it with
qobserva up; your program itself is unaffected. - "Cannot start QObserva: … port … already in use" Another program uses port 8080 or 3000. Free it, or set
QOBSERVA_COLLECTOR_PORT/QOBSERVA_UI_PORT(and point your code at the new collector withQOBSERVA_ENDPOINT). - pyQuil fails on Python 3.13+ Use a Python 3.12 environment; pyQuil doesn't support newer versions yet.
- Windows: "DLL load failed … filename or extension is too long" (D-Wave) Create your virtual environment in a shorter folder path.
More Documentation
For comprehensive guides, examples, and troubleshooting:
- Getting Started: Install, start, and your first observed run
- Testing Guide: Complete setup, deploy, and viewing instructions
- Examples: Full SDK examples with proper project names and tags
- API Reference: Detailed decorator, settings and labeling documentation
- SDK Compatibility: Python and SDK version matrix
- Troubleshooting: Common issues and solutions