Skip to content

Hardware messages

Eleven hardware_* request types manage provider connections and hardware jobs. Unlike the streaming execution messages, every hardware request gets exactly one reply — either its named response type or an error envelope ({"type": "error", "message": "..."} with no code/phase; see errors).

Hardware state is shared across all connections — provider connections, credentials, and the job registry live in one process-wide manager, unlike the per-connection execution state.

Registered provider names: simulator, ibm, google, ionq, nvidia, braket, azure, quantinuum. The kernel connects simulator itself at startup, so the local simulator is always available.

FieldTypeRequiredDefaultNotes
type"hardware_connect"yes
providerstringno""One of the registered provider names.
credentialsobjectno{}Provider-specific string fields.

Reply: hardware_connected with { "provider": ..., "success": true | false }. success: false covers both unknown provider names and failed authentication; an error envelope ("Hardware connect failed: ...") is sent only if the connect attempt raised.

Semantics worth knowing:

  • Synchronous, on the event loop. The connect handshake (which for real providers can include network validation) blocks the kernel until it finishes — expect the connection to be quiet during a slow handshake.
  • Credentials persist on success. They are saved to the OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service), so the next kernel start auto-reconnects without the user re-entering tokens. The frontend can discard its in-memory copy as soon as the ack arrives.
  • Empty credentials are NOT persisted. connect_provider only writes the keyring when the credentials object is non-empty — connecting the simulator with {} never touches the keyring.
  • If no keyring backend exists (headless CI, some containers), the store degrades to in-memory: the session works, persistence doesn’t.

Identical behavior to hardware_connect today (store + connect in one step); it is a separate message type so a future kernel can persist without connecting, without breaking older clients. Same fields, same hardware_connected reply.

FieldTypeRequiredDefault
type"hardware_clear_credentials"yes
providerstringno""

Drops the in-memory connection and wipes the stored keyring entry. Reply: hardware_connected with success: false — the provider is now disconnected.

No fields besides type. Reply: hardware_connected_providers with { "providers": ["simulator", ...] } — used by the frontend to reconcile its UI after a reload. Order is not guaranteed.

This replay-tested session shows set → clear → list:

hardware_credentials.session.json
[
{
"request": {
"type": "hardware_set_credentials",
"provider": "simulator",
"credentials": { "token": "demo" }
},
"responses": [
{ "type": "hardware_connected", "provider": "simulator", "success": true }
]
},
{
"request": {
"type": "hardware_clear_credentials",
"provider": "simulator"
},
"responses": [
{ "type": "hardware_connected", "provider": "simulator", "success": false }
]
},
{
"request": {
"type": "hardware_connected_providers"
},
"responses": [
{ "type": "hardware_connected_providers", "providers": [] }
]
}
]
FieldTypeRequiredDefaultNotes
type"hardware_list_backends"yes
providerstringnonullOmitted/null lists backends from all connected providers. Unknown or disconnected provider → empty list, not an error.

Reply: hardware_backends with { "backends": [BackendInfo, ...] } — see schemas.

FieldTypeRequiredDefaultNotes
type"hardware_submit"yes
providerstringno"simulator"
backendstringno"sim_qasm"Backend/target name from hardware_list_backends.
shotsintegerno1024
codestringno""Source text — not a circuit object.
language"python" | "qsharp"noomittedQ# is also recognized by regex when omitted, for older clients.

Reply: hardware_job_submitted with a JobHandle.

The kernel routes code by language and provider before submission (payload preparation runs off-thread; the provider’s submit call itself runs on the event loop):

SourceProviderRoute
Q#simulatorRaw source passes through — the simulator re-runs it through the executor pipeline.
Q#azureCompiled to QIR; the QIR profile is selected per target — see submitting Q#.
Q#anything elseRejected — those provider SDKs take Python circuit objects, not Q# source or QIR. The error message suggests Azure Quantum or a Qiskit rewrite.
PythonsimulatorRaw source passes through.
Pythonreal hardwareexec() the code, then extract a circuit object from the namespace (Qiskit QuantumCircuit, Cirq Circuit, or CUDA-Q kernel — search order is provider-aware). No recognizable circuit → error.

The local simulator completes synchronously: the simulation runs during the submit call (blocking the kernel’s event loop for its duration), and the job arrives already in status: "complete" with queue_position: null.

FieldTypeRequiredDefault
type"hardware_status"yes
job_idstringno""

Reply: hardware_job_update with the current JobHandle.

Polling semantics — read carefully:

  • Status lookup never re-queries the provider for job status. For queued/running jobs it refreshes only queue_position; the status field changes only via submission, hardware_cancel, or a kernel restart. For real providers, completion is discovered by polling hardware_results, not hardware_status.
  • stale jobs (see below) short-circuit: the stored handle is returned without touching the provider.
  • An unknown job_id is not an error — the kernel synthesizes a stale handle so clients can mark the job as no-longer-tracked instead of surfacing a raw failure:
hardware_status_stale.request.json
{
"type": "hardware_status",
"job_id": "job-from-before-restart"
}
hardware_status_stale.responses.json
[
{
"type": "hardware_job_update",
"job": {
"id": "job-from-before-restart",
"provider": "unknown",
"backend": "unknown",
"status": "stale",
"queue_position": null,
"shots": 0,
"submitted_at": "",
"error": "This job is no longer tracked by the kernel (the kernel may have restarted). Re-submit to run it again."
}
}
]

stale semantics: job metadata persists to disk (~/.nuclei/jobs.json), and on kernel start every job that was non-terminal at shutdown is rehydrated with status: "stale" — the kernel no longer holds the provider SDK handle, so live polling cannot resume; re-submit to run again. Terminal jobs (complete/failed) keep their stored status so history survives restarts.

No fields besides type. Reply: hardware_jobs with { "jobs": [JobHandle, ...] } — every tracked job, including rehydrated ones. The frontend calls this on (re)connect to repopulate its job tracker.

FieldTypeRequiredDefault
type"hardware_results"yes
job_idstringno""

Reply: hardware_result with { "job_id": ..., "data": {...} }. data is a free-form provider dictionary — its shape depends on the provider:

  • The local simulator returns a full SimulationResult for successful jobs, and { "error": KernelError } for failed ones.
  • Real providers return counts-style dictionaries, or { "error": "...", "status": "..." } while the job is still pending or after a failure. Treat data.error as the failure signal.
  • An unknown job_id returns a stale marker rather than an error:
hardware_results_stale.request.json
{
"type": "hardware_results",
"job_id": "job-from-before-restart"
}
hardware_results_stale.responses.json
[
{
"type": "hardware_result",
"job_id": "job-from-before-restart",
"data": {
"error": "Results for this job are no longer available (the kernel may have restarted since it was submitted).",
"status": "stale"
}
}
]
FieldTypeRequiredDefault
type"hardware_cancel"yes
job_idstringno""

Reply: hardware_job_cancelled with { "job_id": ..., "success": true | false }.

Cancel is best-effort and has sharp edges worth knowing:

  • On success the job’s status is set to failed — there is no separate cancelled wire status in protocol v1.
  • Providers whose jobs complete synchronously (simulator, NVIDIA) inherit a no-op cancel that returns true — so cancelling an already-complete simulator job “succeeds” and flips its status to failed. Don’t offer cancel for terminal jobs in your UI.
  • Unknown job ids are treated as already-gone: success: true.
FieldTypeRequiredDefault
type"hardware_dismiss"yes
job_idstringno""

Reply: hardware_job_dismissed with { "job_id": ..., "success": true | false }.

Dismiss is pure bookkeeping: it removes the job’s record from the kernel’s registry and the persistent store (~/.nuclei/jobs.json), so the job stops appearing in hardware_list_jobs — including after a kernel restart.

  • It does not cancel anything provider-side — a running job keeps running. Use hardware_cancel to stop a job; dismiss to drop its record from the tracker.
  • Unknown job ids are treated as already-gone: success: true — the desired end state (“this record is gone”) holds either way.

The full flow as a replay-tested session fixture — submit, dismiss, then hardware_list_jobs confirming the record is gone:

hardware_dismiss.session.json
[
{
"request": {
"type": "hardware_connect",
"provider": "simulator",
"credentials": {}
},
"responses": [
{ "type": "hardware_connected", "provider": "simulator", "success": true }
]
},
{
"request": {
"type": "hardware_submit",
"provider": "simulator",
"backend": "sim_qasm",
"shots": 128,
"code": "from qiskit import QuantumCircuit\n\nqc = QuantumCircuit(2, 2)\nqc.h(0)\nqc.cx(0, 1)\nqc.measure([0, 1], [0, 1])\n",
"language": "python"
},
"responses": [
{
"type": "hardware_job_submitted",
"job": {
"id": "<job-id>",
"provider": "simulator",
"backend": "sim_qasm",
"status": "complete",
"queue_position": null,
"shots": 128,
"submitted_at": "<any>",
"error": null
}
}
]
},
{
"request": {
"type": "hardware_dismiss",
"job_id": "<job-id>"
},
"responses": [
{ "type": "hardware_job_dismissed", "job_id": "<job-id>", "success": true }
]
},
{
"request": {
"type": "hardware_list_jobs"
},
"responses": [
{ "type": "hardware_jobs", "jobs": [] }
]
},
{
"request": {
"type": "hardware_dismiss",
"job_id": "no-such-job"
},
"responses": [
{ "type": "hardware_job_dismissed", "job_id": "no-such-job", "success": true }
]
}
]

The full lifecycle against the always-available local simulator — connect, list, submit, poll, fetch results, list jobs, cancel — as a session fixture the test suite replays end-to-end (the "<job-id>" marker carries the server-generated id between steps):

hardware_simulator.session.json
[
{
"request": {
"type": "hardware_connect",
"provider": "simulator",
"credentials": {}
},
"responses": [
{ "type": "hardware_connected", "provider": "simulator", "success": true }
]
},
{
"request": {
"type": "hardware_connected_providers"
},
"responses": [
{ "type": "hardware_connected_providers", "providers": ["simulator"] }
]
},
{
"request": {
"type": "hardware_list_backends",
"provider": "simulator"
},
"responses": [
{
"type": "hardware_backends",
"backends": [
{
"name": "sim_qasm",
"provider": "simulator",
"qubit_count": 32,
"connectivity": "<any>",
"queue_length": 0,
"average_error_rate": 0.0005,
"gate_set": ["H", "X", "Y", "Z", "CNOT", "CZ", "RX", "RY", "RZ", "T", "S", "Toffoli", "SWAP", "Measure"],
"status": "online"
},
{
"name": "sim_statevector",
"provider": "simulator",
"qubit_count": 24,
"connectivity": "<any>",
"queue_length": 0,
"average_error_rate": 0.0,
"gate_set": ["H", "X", "Y", "Z", "CNOT", "CZ", "RX", "RY", "RZ", "T", "S", "Toffoli", "SWAP"],
"status": "online"
},
{
"name": "sim_noise",
"provider": "simulator",
"qubit_count": 20,
"connectivity": "<any>",
"queue_length": 0,
"average_error_rate": 0.001,
"gate_set": ["H", "X", "Y", "Z", "CNOT", "CZ", "RX", "RY", "RZ", "T", "S", "Measure"],
"status": "online"
}
]
}
]
},
{
"request": {
"type": "hardware_submit",
"provider": "simulator",
"backend": "sim_qasm",
"shots": 256,
"code": "from qiskit import QuantumCircuit\n\nqc = QuantumCircuit(2, 2)\nqc.h(0)\nqc.cx(0, 1)\nqc.measure([0, 1], [0, 1])\n",
"language": "python"
},
"responses": [
{
"type": "hardware_job_submitted",
"job": {
"id": "<job-id>",
"provider": "simulator",
"backend": "sim_qasm",
"status": "complete",
"queue_position": null,
"shots": 256,
"submitted_at": "<any>",
"error": null
}
}
]
},
{
"request": {
"type": "hardware_status",
"job_id": "<job-id>"
},
"responses": [
{
"type": "hardware_job_update",
"job": {
"id": "<job-id>",
"provider": "simulator",
"backend": "sim_qasm",
"status": "complete",
"queue_position": null,
"shots": 256,
"submitted_at": "<any>",
"error": null
}
}
]
},
{
"request": {
"type": "hardware_results",
"job_id": "<job-id>"
},
"responses": [
{
"type": "hardware_result",
"job_id": "<job-id>",
"data": {
"state_vector": "<any>",
"probabilities": {
"00": "<approx:0.5:0.000001>",
"11": "<approx:0.5:0.000001>"
},
"measurements": "<any>",
"bloch_coords": "<any>",
"execution_time_ms": "<any>",
"shot_count": 256,
"metrics": {}
}
}
]
},
{
"request": {
"type": "hardware_list_jobs"
},
"responses": [
{
"type": "hardware_jobs",
"jobs": [
{
"id": "<job-id>",
"provider": "simulator",
"backend": "sim_qasm",
"status": "complete",
"queue_position": null,
"shots": 256,
"submitted_at": "<any>",
"error": null
}
]
}
]
},
{
"request": {
"type": "hardware_cancel",
"job_id": "<job-id>"
},
"responses": [
{ "type": "hardware_job_cancelled", "job_id": "<job-id>", "success": true }
]
},
{
"request": {
"type": "hardware_status",
"job_id": "<job-id>"
},
"responses": [
{
"type": "hardware_job_update",
"job": {
"id": "<job-id>",
"provider": "simulator",
"backend": "sim_qasm",
"status": "failed",
"queue_position": null,
"shots": 256,
"submitted_at": "<any>",
"error": null
}
}
]
}
]

And the failed-submission path — note status: "failed" with error: null on the handle, with the actual diagnostic delivered by hardware_results:

hardware_submit_no_circuit.session.json
[
{
"request": {
"type": "hardware_submit",
"provider": "simulator",
"backend": "sim_qasm",
"shots": 128,
"code": "x = 1\n",
"language": "python"
},
"responses": [
{
"type": "hardware_job_submitted",
"job": {
"id": "<job-id>",
"provider": "simulator",
"backend": "sim_qasm",
"status": "failed",
"queue_position": null,
"shots": 128,
"submitted_at": "<any>",
"error": null
}
}
]
},
{
"request": {
"type": "hardware_results",
"job_id": "<job-id>"
},
"responses": [
{
"type": "hardware_result",
"job_id": "<job-id>",
"data": {
"error": {
"code": "unsupported_framework",
"message": "No supported quantum framework detected in code.",
"traceback": null,
"framework": null,
"dependency": null
}
}
}
]
}
]
illustrative/hardware_connect_azure.request.json
{
"type": "hardware_connect",
"provider": "azure",
"credentials": {
"subscription_id": "00000000-0000-0000-0000-000000000000",
"resource_group": "my-quantum-rg",
"workspace_name": "my-workspace",
"location": "eastus"
}
}
illustrative/hardware_connect_azure.responses.json
[
{
"type": "hardware_connected",
"provider": "azure",
"success": true
}
]

A Q# submission to a Quantinuum target via Azure compiles to QIR with the Adaptive_RI profile and comes back queued. queue_position is null at submission; Azure does not expose queue positions, so later hardware_status polls report -1 (“unknown”) rather than a real position:

illustrative/hardware_submit_azure_qsharp.request.json
{
"type": "hardware_submit",
"provider": "azure",
"backend": "quantinuum.sim.h1-1e",
"shots": 500,
"code": "operation Main() : Result[] {\n use qs = Qubit[2];\n H(qs[0]);\n CNOT(qs[0], qs[1]);\n return [M(qs[0]), M(qs[1])];\n}\n",
"language": "qsharp"
}
illustrative/hardware_submit_azure_qsharp.responses.json
[
{
"type": "hardware_job_submitted",
"job": {
"id": "8f4b8f6e-1c2d-4e5f-9a0b-3c4d5e6f7a8b",
"provider": "azure",
"backend": "quantinuum.sim.h1-1e",
"status": "queued",
"queue_position": null,
"shots": 500,
"submitted_at": "2026-06-10T12:00:00.000000+00:00",
"error": null
}
}
]