Skip to content

Workflow Authoring Quickstart ​

Use a QDash flow template to define targets, an explicit sequence of calibration steps, and the parameters those steps need.

To create, save, and run a workflow in the application, see Creating Workflows.

Start from an Existing Template ​

Copy the closest file under src/qdash/workflow/templates/ before writing a flow from scratch. Templates contain the terminal hooks, target validation, task order, and defaults required by the current runtime. Their matching tests under tests/qdash/workflow/ protect those decisions. A template the editor should offer is registered in templates/templates.json with the backend its tasks run on (qubex or fake). GET /flows/templates?backend=fake returns only the fake templates, and the New Flow page requests the list with the selected backend, so its selector never offers a hardware template on a fake deployment; a request without backend still returns every template.

The smallest useful flow has four parts:

  1. A Prefect @flow with QDash terminal hooks.
  2. A Target selected from MUX IDs or explicit qubit IDs.
  3. An explicit list of task names inside one or more Step objects.
  4. A CalibService that runs those steps.

Define a Flow ​

This example follows the structure of templates/coherence_check.py. It assumes the selected qubits already have the calibration inputs required by CheckT1 and CheckRamsey.

python
from typing import Any

from prefect import flow

from qdash.workflow.service import CalibService
from qdash.workflow.service.calib_service import (
    on_flow_cancellation,
    on_flow_crashed,
    on_flow_failure,
)
from qdash.workflow.service.steps import CustomOneQubit
from qdash.workflow.service.targets import MuxTargets, QubitTargets, Target

TASKS = ["Configure", "CheckT1", "CheckRamsey"]


@flow(
    on_cancellation=[on_flow_cancellation],
    on_failure=[on_flow_failure],
    on_crashed=[on_flow_crashed],
)
def coherence_sample(
    username: str,
    chip_id: str,
    mux_ids: list[int] | None = None,
    exclude_qids: list[str] | None = None,
    qids: list[str] | None = None,
    tags: list[str] | None = None,
    flow_name: str | None = None,
    project_id: str | None = None,
) -> Any:
    targets: Target
    if mux_ids is not None:
        targets = MuxTargets(mux_ids=mux_ids, exclude_qids=exclude_qids or [])
    elif qids is not None:
        targets = QubitTargets(qids=qids)
    else:
        raise ValueError("mux_ids or qids is required; select targets before running this flow")

    steps = [
        CustomOneQubit(
            step_name="coherence_sample",
            tasks=TASKS,
            mode="synchronized",
        )
    ]
    service = CalibService(
        username,
        chip_id,
        flow_name=flow_name,
        tags=tags,
        project_id=project_id,
    )
    return service.run(targets, steps)

Keep username, chip_id, flow_name, and project_id in the signature. QDash injects them when it starts a saved flow. Target parameters remain optional in the signature so the same flow can accept a MUX selection or an explicit qubit selection, but the function must reject a run with neither.

Choose Targets ​

TargetUse whenResolution
MuxTargetsHardware scheduling starts from MUX IDsExpands each MUX to four qubits and removes exclude_qids
QubitTargetsThe caller selected individual qubitsUses the supplied qubit IDs
CouplingTargetsThe flow starts from directed two-qubit pairsExposes both coupling IDs and their endpoint qubits
AllMuxTargetsThe flow targets a 64-qubit layout's MUX IDs 0–15Expands that fixed MUX set and applies exclusions

When both mux_ids and qids are supplied, standard templates prefer mux_ids. Preserve that rule across templates so UI and scheduled executions resolve the same request consistently.

Compose Steps ​

CalibrationStep performs hardware work and receives its own QDash execution record. TransformStep filters or schedules existing context without creating an execution record. Pipeline validates each step's requires and provides fields before the first step runs.

Keep task lists in the template rather than importing them from another template or relying on a step's default list. The visible order is part of the flow contract: repeated measurements and configuration checkpoints must remain reviewable in the saved source. If two templates must stay aligned, compare their lists in template tests.

State Between Steps ​

CalibService._run_pipeline() passes the returned StepContext to each step in order. Target selection and scheduling travel through this context; Qubex calibration inputs resolve from the current calibration database when each task preprocesses.

StateOwnerConsumer
Remaining qubitsStepContext.candidate_qidsFilters, scheduling, and calibration steps
Candidate coupling IDsStepContext.candidate_couplingsSteps that consume coupling candidates
CR parallel groupsStepContext.metadata["cr_schedule"]CustomTwoQubit and TwoQubitCalibration
Typed step resultsStepContext.one_qubit_check, one_qubit_fine_tune, two_qubitFilters and pipeline results
Accepted calibration valuesMongoDB qubit and coupling calibration recordsDeclared task inputs
Experiment settings and overridesCalibService.default_run_parameters and task_parametersTask construction and isolated worker sessions

Each hardware step gets its own Execution. After a hardware step finishes, the pipeline creates a new orchestrator and session for the next hardware step; transform steps retain the context without opening an Execution. Calibration values survive this boundary because applied outputs are persisted, rather than copied from the preceding step's return value into the next task.

For normal Qubex tasks, QubexTask._load_parameters_from_db() reads declared inputs from the selected qubit or, for coupling tasks, the control qubit, target qubit, and coupling records according to qid_role. Explicit input overrides take precedence. Measurement-only outputs do not replace accepted inputs for later tasks. Run parameters such as shots and sweep ranges come from task and workflow settings. See Parameter Resolution for precedence and snapshot re-execution, and Calibration Data Lifecycle for output promotion.

Parallel coupling workers receive the shared settings and parent Execution ID in session_config, then create isolated sessions. Within each coupling, tasks run sequentially; workers do not share Python objects. See CR Gate Scheduler for schedule reuse and group ordering.

The context and session lifecycle are implemented in service/steps/pipeline.py and service/calib_service.py; worker session construction is in service/_internal/scheduling_tasks.py, all under src/qdash/workflow/. When changing these boundaries, run the pipeline and worker tests under tests/qdash/workflow/service/ using the Workflow Testing guidance.

For the user-facing procedure, see Use Earlier Calibration Results in Later Steps.

Set Task Parameters ​

Pass shared run-parameter fallbacks through default_run_parameters and settings for one task through task_parameters:

python
from qdash.workflow import sweep

service = CalibService(
    username,
    chip_id,
    project_id=project_id,
    default_run_parameters={
        "interval": 150 * 1024,
    },
    task_parameters={
        "CheckRamsey": {
            "run": {
                "shots": 1024,
                "time_range": sweep.arange(0, 10_000, 100),
            },
            "input": {"control_frequency": 7.012},
            "update_calibration_parameters": False,
        },
    },
)

default_run_parameters and task_parameters[task_name]["run"] use the same value syntax. run sets run parameters the task declares in its run_spec, and overrides shared defaults. input replaces calibration inputs the task declares in its input_spec; the value applies to every qid the task runs on. Use sweep.linspace, sweep.logspace, sweep.arange, or sweep.range for generated run ranges, sweep.values(...) for explicit points, and plain values for other run values and input entries. The helper records the constructor without creating an array in the workflow process, and a parameter that declares one sweep constructor accepts any of them. Setting update_calibration_parameters=False retains task history without promoting validated outputs to the current calibration state. A name in the wrong section, or one the task does not declare, fails when the task is created. The complete precedence rules are in Parameter Resolution.

Preserve Terminal Handling ​

Every top-level flow that owns hardware reservations must register on_flow_cancellation, on_flow_failure, and on_flow_crashed. Do not reproduce their exception-classification logic in the template. The system single-task-executor also owns its reservation and uses these hooks. Use the *_keep_lock variants only for nested flows configured with use_lock=False that borrow the parent flow's reservation.

The hooks finalize execution and task records that the worker cannot close itself and release only reservations owned by that flow. Workflow Engine Architecture explains the two cancellation paths.

Test the Template ​

Template tests replace CalibService with a recording fake and call the flow function directly. Assert target precedence, required-target errors, step types, task order, and parameter dictionaries. This catches authoring errors without Prefect infrastructure or laboratory hardware.

Run the matching test first, then the workflow suite:

bash
uv run pytest tests/qdash/workflow/test_coherence_check_template.py -v
task test-workflow

Use Workflow Testing to choose broader checks. Saving, registering, and dispatching the finished flow crosses the boundaries described in Workflow Services.

Released under the Apache 2.0 License.