Skip to Content
DocsTroubleshootingScript Debugging

Script Debugging

Guide to debugging quantum scripts and workflows on the Marqov platform.


Temporal Sandbox Restrictions

Temporal enforces a deterministic sandbox on workflow code. This has direct implications for how Marqov scripts are structured.

What you cannot do in a @workflow function

  • Import marqov or any marqov submodule
  • Import third-party libraries (numpy, qiskit, etc.)
  • Make network calls or access the filesystem
  • Use datetime.now() or other non-deterministic operations

What you can do in a @workflow function

  • Call @task-decorated functions (these become Temporal activities)
  • Use built-in Python types (str, int, dict, list)
  • Perform pure computation on primitive types

Correct pattern

from marqov import task, workflow @task def run_circuit(theta): # All marqov imports and heavy computation happen here from marqov import Circuit, LocalExecutor circuit = Circuit().ry(theta, 0).cnot(0, 1) executor = LocalExecutor() import asyncio result = asyncio.run(executor.execute(circuit, shots=1000)) return result.counts @workflow def vqe_step(theta): # This function only composes tasks -- no marqov imports counts = run_circuit(theta) return counts

Incorrect pattern (will fail)

from marqov import task, workflow, Circuit # Circuit import here is fine at module level @workflow def vqe_step(theta): circuit = Circuit().ry(theta, 0) # FAILS: Circuit() in workflow sandbox counts = run_circuit(circuit) return counts

Serialization Requirements

All data passed between @task and @workflow functions must be JSON-serializable, because Temporal serializes activity inputs/outputs as JSON.

Supported types

  • str, int, float, bool, None
  • list, dict (with serializable values)

Types that will fail

  • marqov.Circuit — use circuit.to_dict() / Circuit.from_dict(data)
  • numpy.ndarray — convert to list with .tolist()
  • ExecutionResult — extract .counts (a dict)
  • Custom classes — convert to dict

Example: passing a circuit through Temporal

@task def build_circuit(theta): from marqov import Circuit circuit = Circuit().ry(theta, 0).cnot(0, 1) return circuit.to_dict() # Returns JSON-serializable dict @task def run_circuit(circuit_dict, shots): from marqov import Circuit, LocalExecutor circuit = Circuit.from_dict(circuit_dict) # Reconstruct import asyncio result = asyncio.run(LocalExecutor().execute(circuit, shots=shots)) return result.counts @workflow def my_workflow(theta): circuit_data = build_circuit(theta) counts = run_circuit(circuit_data, 1000) return counts

Using the Temporal UI for Debugging

The Temporal UI provides workflow history, input/output inspection, and retry controls.

Accessing the UI

  • Local: http://localhost:8088
  • Production: Configured via NEXT_PUBLIC_TEMPORAL_UI_URL

Key views

  1. Workflow list: See all running and completed workflows. Filter by status, workflow type, or time range.

  2. Workflow detail: Click a workflow to see:

    • Input parameters (the serialized transport graph)
    • Execution timeline
    • Each activity (task) execution with inputs, outputs, and timing
    • Error details for failed activities
  3. Activity failures: Click a failed activity to see:

    • The error message and stack trace
    • Input arguments that caused the failure
    • Retry history

Debugging a failed workflow

  1. Find the workflow by ID (shown in the job detail page or logged by the worker).
  2. Open the workflow in the Temporal UI.
  3. Look at the event history for ActivityTaskFailed events.
  4. Inspect the failure field for the error message and stack trace.
  5. Check the activity input to understand what data was passed.

Common Pitfalls

Angle units are radians

All rotation gates (rx, ry, rz) use radians, not degrees. A common mistake:

# Wrong: 90 degrees circuit.rx(90, 0) # Correct: pi/2 radians import math circuit.rx(math.pi / 2, 0)

Qubit ordering varies by backend

Different backends use different qubit ordering conventions:

SystemConventionQubit 0
QASM / QiskitBig-endianMost significant bit
qulacs-wasm (browser)Little-endianLeast significant bit
BraketBig-endianMost significant bit

When using the browser simulator with qulacs-wasm, bitstrings in measurement results are reversed compared to QASM/Qiskit convention. The platform handles this conversion automatically, but be aware of it when comparing results manually.

Measurement is implicit in Braket

When using MarqovDevice.run() with a Braket backend, all qubits are automatically measured. You do not need to add explicit measurement operations to your circuit. For Azure backends, measure_all() is added automatically if no classical registers exist.

cloudpickle version mismatch

Tasks are serialized using cloudpickle. If the cloudpickle version differs between the dispatch environment and the worker environment, deserialization may fail with:

_pickle.UnpicklingError: invalid load key

Ensure the same cloudpickle version is installed in both environments.

Large circuit serialization

Very large circuits (thousands of gates) may produce large serialized payloads that exceed Temporal’s default payload size limit (2 MB). For large circuits:

  1. Use circuit.to_openqasm() instead of circuit.to_dict() for more compact serialization.
  2. Or increase Temporal’s payload size limit via dynamic configuration.

Direct vs Temporal Execution

Modeexecution_modeWhen to use
Direct"direct"Simple, single-task jobs. Faster startup. Default mode.
Temporal"temporal"Multi-task workflows, jobs needing retry/durability, long-running jobs

Direct mode executes the script synchronously in the worker process. Temporal mode dispatches a workflow that executes tasks as Temporal activities with parallelization and retry support.

Last updated on