--- url: /quickstart.md description: >- Crash course covering CLI installation, Quantum SDK setup, and running a full quantum workflow from development to deployment on Kipu Quantum Hub. --- # Quickstart This page provides a crash course on using Kipu Quantum Hub to run an entire quantum workflow, from development to deployment :rocket:. First of all, [create an account](https://hub.kipu-quantum.com) if you don't have one yet. :::details TL;DR To get started quickly, install the CLI and the Quantum SDK in a Python virtual environment: ```bash npm install -g @quantum-hub/qhubctl uv add qhub-quantum qhubctl login -t YOUR_PERSONAL_ACCESS_TOKEN_HERE ``` To run a Qiskit program, all you need is an account and three lines of Quantum code: ```python from qhub.quantum.sdk import HubQiskitProvider qiskit_circuit = ... # create your Qiskit circuit here # Either use the CLI to log in or set the environment variable KQH_PERSONAL_ACCESS_TOKEN. provider = HubQiskitProvider() # Alternatively, you can pass the access token as an argument to the constructor provider = HubQiskitProvider(access_token="YOUR_PERSONAL_ACCESS_TOKEN_HERE") result = provider.get_backend("kipu.sim.qsim").run(qiskit_circuit, shots=100).result() ``` You can find more examples and documentation on how to use different backends etc. in the [Quantum SDK documentation](sdk-quantum.md). ::: ## Install the CLI To install the CLI, you must install Node.js 20 or higher and the `npm` command line interface using either a [Node version manager](https://github.com/nvm-sh/nvm) or a [Node installer](https://nodejs.org/en/download). Then install the CLI using `npm`: ```bash npm install -g @quantum-hub/qhubctl ``` > \[!TIP] > Take a look at the [CLI reference](cli-reference) for more information on the available commands. ### Login to your account Copy your [personal access token](https://dashboard.hub.kipu-quantum.com) to your clipboard. Login to your account using your access token: ```bash qhubctl login -t ``` > \[!TIP] > If you like to work in the context of an organization you joined on Kipu Quantum Hub, run > `qhubctl set-context` and select the organization you want to work with. ## Run your first quantum program The Quantum SDK provides an easy way to develop quantum circuits that can be executed on quantum backends/devices available through Kipu Quantum Hub. The SDK allows you to use your favorite quantum libraries, such as Qiskit and AWS Braket, to construct circuits and run them on quantum hardware in the cloud. ::: warning IMPORTANT You need Python 3.11 or higher to use the Quantum SDK. ::: :::tip Instead of using your global Python installation, we recommend creating a dedicated Python virtual environment for your quantum projects. We recommend using [uv](https://docs.astral.sh/uv/) to create and manage Python virtual environments easily. You can use [venv](https://docs.python.org/3/library/venv.html), [virtualenv](https://virtualenv.pypa.io/en/latest/), or [conda](https://docs.conda.io/projects/conda/en/stable/) as well. ::: ### Set up a new project \[optional] This step is optional but recommended to keep your project dependencies isolated. Create a new directory for your quantum project and navigate into it: ```bash mkdir ~/my-quantum-project cd ~/my-quantum-project ``` We recommend using `uv` to create a new virtual environment in the project directory. This helps to keep your project dependencies isolated from your global Python installation. If you don't have `uv` installed, you can install it as described in the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation). Then create a new Python virtual environment using `uv`: ```bash uv venv # Create a new virtual environment in the .venv directory uv init # Initializes a new uv project ``` Activate the virtual environment: ```bash source .venv/bin/activate ``` ### Install the Quantum SDK: You can install the Quantum SDK using `uv`: ::: tabs key:uvPip \== uv ```shell uv add qhub-quantum ``` \== pip ```bash pip install --upgrade qhub-quantum ``` ::: ### Example: Coin Toss To construct a Qiskit circuit, i.e., a quantum algorithm, that simulates `n` coin tosses on a quantum computer, representing outcomes as zeros `0` and ones `1` instead of heads and tails. This results in `2^n` possible outcomes, and each measurement (shot) of the quantum state yields one of these possibilities. To run the circuit on a quantum backend via Kipu Quantum Hub, we use the `HubQiskitProvider` from our Quantum SDK. This Qiskit provider connects to Kipu Quantum Hub, enabling execution of quantum circuits on supported devices. Learn more about Qiskit [here](https://qiskit.org/). > \[!TIP] > We recommend using the CLI to log in and authenticate with Kipu Quantum Hub. > Then, you can use the `HubQiskitProvider` without specifying the access token: > > ```python > provider = HubQiskitProvider() > ``` Add the following code to a new Python file, e.g., `coin_toss.py`: ```python from qhub.quantum.sdk import HubQiskitProvider from qiskit import QuantumCircuit, transpile n_coin_tosses = 2 circuit = QuantumCircuit(n_coin_tosses) for i in range(n_coin_tosses): circuit.h(i) circuit.measure_all() # Use qhubctl and log in with "qhubctl login" or set the environment variable KQH_PERSONAL_ACCESS_TOKEN. # Alternatively, you can pass the access token as an argument to the constructor provider = HubQiskitProvider() ### If you do not want to log in using the CLI, you can also provide your personal access token directly here: # provider = HubQiskitProvider(access_token="YOUR_PERSONAL_ACCESS_TOKEN_HERE") # Select a quantum backend suitable for the task. All KQH supported quantum backends are # listed at https://hub.kipu-quantum.com/quantum-backends. backend = provider.get_backend("kipu.sim.qsim") # Transpile the circuit ... circuit = transpile(circuit, backend) # ... and run it on the backend job = backend.run(circuit, shots=100) counts = job.result().get_counts() print(counts) ``` You can run the program like this: ```bash python coin_toss.py ``` A possible outcome of the coin toss quantum algorithm could be: ```json { "00": 23, "01": 22, "10": 29, "11": 26 } ``` > \[!TIP] > We have prepared a Jupyter notebook that you can use to immediately run the Quantum Coin Toss example: > Navigate to the [notebooks/coin\_toss.ipynb](https://dashboard.hub.kipu-quantum.com/community/implementations/1a0ae675-4b23-405c-af8e-f4189ff14e0f) > \[!IMPORTANT] > Access to Kipu quantum simulator `kipu.sim.qsim` and Azure IonQ Simulator `azure.ionq.simulator` is free of charge. > Other backends/devices requires an account with active payment information. ## Create your first Service project Create a new project by running the following command: ```bash qhubctl init ``` You will be prompted to provide some information about your project. For this quickstart, select the following configuration: * **Service name**: `my-project` * **Starter template**: `Python Starter` * **vCPU configuration**: `1 vCPU` * **Memory configuration**: `1GB` This will create a new directory called `my-project` containing all required files to run your quantum code on Kipu Quantum Hub. The `Python Starter` templates implement the coin toss example from above as a Service. Note the `qhub.json` file in the project directory, which contains the project configuration, for example: ```json { "name": "my-project", "descriptionFile": "README.md", "resources": { "cpu": 1, "memory": 1 }, "runtime": "PYTHON_TEMPLATE" } ``` > \[!TIP] > For more information on the bootstrapped project structure, see the `README.md` file in the project directory. ### Test your service locally Let's test your service locally before deploying it to Kipu Quantum Hub. First, switch to your project directory: ```bash cd my-project ``` Then, install the required dependencies. We recommend creating a dedicated Python environment to install and track all required packages from the start. You may use the `requirements.txt` file to create a virtual environment with the tooling of your choice. For example, you can use `uv` to create a virtual environment and install the required packages: ```bash uv venv uv sync source .venv/bin/activate ``` Open the `src/program.py` file, modify the following line and enter your personal access token: ```python provider = HubQiskitProvider(access_token="YOUR_PERSONAL_ACCESS_TOKEN_HERE") ``` Finally, run your service locally: ```bash python -m src ``` The output should look like this: ```json { "counts": { "000": 6, "001": 13, "010": 19, "011": 18, "100": 8, "101": 14, "110": 8, "111": 14 }, "elapsed_time": 17.837932109832764 } ``` ### Test your service locally using qhubctl To begin, navigate to your project directory: ```bash cd my-project ``` Next, run the following command: ```bash qhubctl serve ``` Once the server is operational, you can access . This interface provides you the ability to run your current code and see the results. Further information can be found in the [qhubctl reference](cli-reference#qhubctl-serve). Open the `POST /` operation and click on the "Try it out" button. Paste the following JSON into the request body field: ```json { "data": { "n_coin_tosses": 2 } } ``` Click on the "Execute" button to run the service. The response body will contain the ID of the service execution. Copy the ID to your clipboard. Open the `GET /{id}` operation and click on the "Try it out" button. Paste the ID into the `id` field and click on the "Execute" button. You can use this endpoint to check the status of the service execution. If the status is `SUCCEEDED`, you can retrieve the result. Open the `GET /{id}/result` operation and click on the "Try it out" button. Paste the ID into the `id` field and click on the "Execute" button. The response body will contain the result of the service execution, similar to this: ```json { "counts": { "10": 30, "11": 25, "00": 24, "01": 21 }, "elapsed_time": 11.129297733306885, "_links": { "status": { "href": "/5b25134c-dd05-47a0-9c12-ff9816074936" } }, "_embedded": { "status": { "id": "5b25134c-dd05-47a0-9c12-ff9816074936", "status": "SUCCEEDED", "created_at": "2025-03-24 11:16:20", "started_at": "2025-03-24 11:16:20", "ended_at": "2025-03-24 11:16:32" } } } ``` Press Ctrl+C to stop the local server. ### Deploy your service To deploy your service to Kipu Quantum Hub, run the following command in your project directory: ```bash qhubctl up ``` This will compress your project directory and upload it. After a successful deployment, you will find the service in the [Services](https://dashboard.hub.kipu-quantum.com/services) section. Alternatively, you can create a ZIP file of your project and upload it manually to the platform: ```bash qhubctl compress ``` Both commands, `qhubctl up` and `qhubctl compress`, consider the definitions from the `.qhubignore` file (gitignore syntax) to exclude files and directories from being uploaded or compressed. `.git`, `node_modules`, `.venv`, `__pycache__`, and `service.zip` are always excluded. > **Note:** `.qhubignore` uses gitignore syntax, so to ship a directory's contents you must re-include both the > directory and its contents — e.g. `*`, then `!src`, `!src/**`, `!Dockerfile`. ### Execute your service Execute your service with the example input data stored in `input/data.json` and `input/params.json` by running the following command: ```bash qhubctl run ``` After a successful execution, the output should look like this: ``` Running Job (a7a3422b-9522-408b-96c9-32cdb497b12b)... Job succeeded. See result at https://dashboard.hub.kipu-quantum.com/jobs/a7a3422b-9522-408b-96c9-32cdb497b12b ``` For more details and options of the `qhubctl run` command, see the [CLI reference](cli-reference#qhubctl-run-serviceid). ## What's next? --- --- url: /implementations/introduction.md description: >- Overview of Implementations, the Git-backed repositories on Kipu Quantum Hub for storing, versioning, and collaborating on quantum code. --- # Introduction Implementations are hosted as Git repositories, which means that version control and collaboration are core elements of the platform. In a nutshell, an implementation (also known as a repo or repository) is a place where code and assets can be stored to back up your work, share it with the community, and work in a team. In these pages, you will go over the basics of getting started with Git and interacting with implementations on the platform. ## What's next? * [Getting Started](getting-started) * [Implementation Settings](settings) * [Create a Service](create-a-service) --- --- url: /implementations/getting-started.md description: >- Create an implementation repository, clone it locally with a personal access token, and push your first commit to Kipu Quantum Hub. --- # Getting started This beginner-friendly guide will teach you the basic skills you need to create and manage your first implementation. ## Requirements This guide assumes that you have Git installed on your machine. If you do not have git available as a CLI command yet, you will need to install Git for your platform. ## Creating an implementation To create a new Implementation, visit the [Create Implementation](https://dashboard.hub.kipu-quantum.com/v2/implementations/new) page. Enter a name for your implementation and click on the "Create" button. After creating the implementation, you should see a page like this: Note that the lock icon indicates that the implementation is *private*. Learn how to make your implementation *public* in the [settings](settings) section. Moreover, the empty implementation page provides you a set of command line instructions to clone the repository and start working on your implementation. In the following we will go through the steps to clone the repository and add a README file. ## Cloning the repository locally Downloading the implementation to your local machine is called cloning. You can clone the implementation and navigate to it using the following commands: ```bash git clone https://qhub:@repository.hub.kipu-quantum.com//.git cd ``` Best **just copy the clone command** from the command line instructions on the implementation page. This command already contains the **correct URL** to clone the repository. ::: tip Authentication The platform Git Server supports HTTPS with basic authentication. You can authenticate by providing your personal access token in the Git URL. You can clone any repository that you have at least 'Viewer' permissions for. Learn more about permissions in the [settings](settings) section. ::: ## Add a README Now let's add a README file to your repository that provides information about your implementation. Feel free to add some markdown content to the README file. ```bash touch README.md ``` ## Push your changes You can use Git to save new files and any changes to already existing files as a bundle of changes called a *commit*, which can be thought of as a “revision” to your project. In order to sync the new commit with the platform, you then *push* the commit. Push your code using the following commands: ```bash ## Create any file you like and add some content! Then... git add . git commit -m "add README" git push --set-upstream origin main ``` That's it! After refreshing your implementation page, you will see all your recently added files. For example in the screenshot below the user added its implementation for a [Managed Service](../services/managed/introduction). Note that one of the files is a Dockerfile. In case a Dockerfile is present in the implementation a "Create Service" button is available in the UI to [create a Service](create-a-service.md) based on the implementation. ## What's next? * Learn how to [manage permissions](settings). * Learn how to [share your implementation with the community](settings). * Learn how to [create a Service](create-a-service). --- --- url: /implementations/settings.md description: >- Manage implementation visibility and assign Viewer, Maintainer, or Owner roles to collaborators on Kipu Quantum Hub. --- # Implementation Settings In this section you will learn how to manage the settings of your implementation. ## Change visibility When you create your implementation it is *private* by default. Unless the implementation is owned by an [organization](../manage-organizations), you are the only one who can see your implementation and update any code. In the settings of your implementation you can change the visibility to *public*. *Public* means that all Kipu Quantum Hub users can see your implementation and its code. However, they are not allowed to make any changes to your implementation. ## Managing Members If you want to grant certain users access to your implementation, you can add them as members. A member can have the role *Viewer*, *Maintainer* or *Owner*. * **Viewer**: Can see the implementation and its code, but cannot make any changes. * **Maintainer**: Has all the rights of a Viewer and can make changes to the code. * **Owner**: Has all rights of a Maintainer and can manage the implementation settings, e.g., adding users, changing the visibility, and deleting the implementation. --- --- url: /implementations/create-a-service.md description: >- Bootstrap a Docker Python starter project, push it to an implementation, and turn it into a Managed Service using qhubctl. --- # Create a Service This step-by-step guide will teach you how to create a service based on your implementation. You will learn how to initialize a Docker Python project using the [CLI](../cli-reference), push its code to an implementation and create a managed service based on it. This guide assumes that you already know the basics about [managed services](../services/managed/introduction) and [implementations](../implementations/getting-started). ## Requirements This guide assumes that you have the latest version of the CLI installed on your machine. If not you can install it by following the instructions in the [CLI reference](../cli-reference.md). ## Initialize a Docker Python Starter Project The CLI provides you with a set of starter templates to help you get started quickly with your quantum services. You can see the full list of available templates in our [Implementations](https://dashboard.hub.kipu-quantum.com/community/implementations). In this tutorial we will use the **Docker Python Starter** template to create a new service based on custom Docker containers. To initialize the project, run the following command in your terminal: ```bash qhubctl init ``` In the interactive prompt: * Choose a name, e.g., `my-service`. * Select `Docker Starter` as starter template. * Choose your resource configuration, e.g., accept the defaults. After the initialization, you will find a new folder with the name of your service in the current directory. For sake of simplicity, we will not get into details of the generated code in this tutorial. You can check out the README file in the generated project for more information. ### Create an implementation and push the code Next, [create a new Implementation](https://dashboard.hub.kipu-quantum.com/v2/implementations/new) and upload the code of your service. You can follow the steps in the [Getting Started](../implementations/getting-started) guide. After refreshing your implementation page, you should see all your added files. Similar to the screenshot below. ### Create a service based on the implementation As your service code includes a Dockerfile, you should see the "Create Service" button in the action bar of your implementation page. Click on the button to create a new service based on your implementation. Navigate to the [Services](https://dashboard.hub.kipu-quantum.com/services) page to see your new service. Congratulations, you have successfully created a service based on your implementation 🎉. --- --- url: /services/managed/introduction.md description: >- Overview of Managed Services, the containerized on-demand runtime for deploying quantum code to Kipu Quantum Hub via CLI or UI. --- # Introduction Managed Services enable you to run your quantum code on-demand without needing to manage your own infrastructure, provision servers, or upgrade hardware. We containerize and deploy your quantum code fully automatically and make it accessible through well-known protocols (HTTP/S) – you bring the code, we do the REST. We enable developers to focus on writing their quantum code in Python (and other languages) to build quantum solutions for tomorrow's challenges. A Managed Service consists of your quantum code, metadata describing the service, and configuration information for the execution of the service. By using our coding templates, you can easily turn your ideas into running quantum solutions at rapid speed. Once deployed, you can asynchronously execute your service and retrieve the results. Further, you can share your services with your colleagues or even external parties, everything through an [HTTP API](openapi). > \[!TIP] Quickstart Guide > Check out our [quickstart](../../quickstart) guide to get started with Managed Services using the CLI. ## Create a Managed Service You can create a Managed Service either via the [CLI](../../cli-reference) or via the [create service page](https://dashboard.hub.kipu-quantum.com/services/new) of our UI. ### Using the CLI (recommended) We strongly recommend to use the [CLI](../../quickstart) to create new Managed Service. You could select a general starter template or choose one specifically for a certain quantum provider or backend. Furthermore, it provides you with commands to directly package and deploy your quantum code along with the metadata and configuration. > \[!TIP] Check out the README > Take a look into the `README.md` file of your created project to get started. > It contains all the information you need to run and test your code locally as well as to deploy it to the platform. ### Using the platform UI On the [create service page](https://dashboard.hub.kipu-quantum.com/services/new) of our UI you can create a new Managed Service by uploading a ZIP file containing your quantum code. You have to zip (at minimum) the `src` folder and the `requirements.txt` file from your project folder. **You must not zip the project folder itself but its content.** You may execute the following from within the project folder: `zip -r qhub.zip src requirements.txt`. Alternatively, you can use `qhubctl compress` to create the `qhub.zip` file as well. Now that you have your code in a zip-file, fill out the form and import the `qhub.zip` file you created before. And there you go. As soon as the containerization of your code has finished you are able to run jobs against your service. Further, you may publish it for internal use or into the Kipu Quantum Hub Marketplace to share it with other users. ## Service Metadata The following table describes the metadata properties of a service. | Property | Description | |--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Name | Choose a meaningful name for your service. If you publish your service later on, this name will be displayed to other users. | | Service Type | Select "Managed Services" and upload your code archive (ZIP). The option "On-premise Service" can be used if your service is running somewhere (e.g., on your own infrastructure) and you just want Kipu Quantum Hub to manage the access to it. | | Description | Other users will see this description of the service, if its name sparked some interest, and they clicked on it in the marketplace. So any additional information you want to provide goes in here. | ## Service Configuration The following table describes the configuration capabilities of a service. | Property | Description | |------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Runtime Configuration | Kipu Quantum Hub supports to run your service implemented in Python (`Python Template`) or based on custom Docker containers (`Docker`). For example, choose "Python Template" if you selected `Python Starter` when creating your project using the CLI. Choose "Docker" if you have selected the `Docker Starter` template. | | Resource Configuration | Define and configure the allocated resources when your service is executed. You can define the number of virtual CPU cores and the amount of memory in GB to be allocated for your service at runtime. If you wish to access GPU resources, you may specify the GPU configuration for your service. | | API Specification | Click on "Browse" if you have prepared an OpenAPI-based description for your service. You can leave this empty to use the platform's default template. You can change this later on the service details page. | > \[!TIP] Describe your Service API > Further details and a template to create your dedicated API description can be found [here](openapi). --- --- url: /services/managed/service-configuration.md description: >- Configure runtime, resources, provider access tokens, and environment variables for a deployed Managed Service. --- # Service Configuration ## Runtime Configuration After you have created a new service, you can change the runtime configuration on the service detail page. From the toolbar on the top right, under the "Edit" section click "Runtime Configuration". You can change the runtime, to be either "Python Template" or "Docker". You find more information on the [Runtime Interface](runtime-interface) page. Additionally, you can choose to add your configured provider access tokens to the runtime as environment variables. If you enable this option, the following environment variables are set depending on the backend provider: * IBM Quantum: `QISKIT_IBM_TOKEN` (API key value) * IBM Cloud: `QISKIT_IBM_TOKEN` (API key value), `QISKIT_IBM_INSTANCE` (Service CRN value), `QISKIT_IBM_CHANNEL` (constant value: ibm\_cloud) * D-Wave Leap: `DWAVE_API_TOKEN` (API key value) The Qiskit and D-Wave Ocean SDK may require special instrumentation to use these variables. Further, you can add additional environment variables to store other API keys, configuration values, or secrets. You can access them in your code like regular environment variables, for example with `os.getenv()` in Python. ## Resource Configuration After you have created a new service, you can change the resource configuration on the service detail page. --- --- url: /services/managed/openapi.md description: >- Document Managed Service endpoints, inputs, and outputs using OpenAPI v3 so users understand how to call and consume your API. --- # Describe your API using OpenAPI Specification v3.0 A proper API description will help users of your service to understand how to use it. It is the technical interface of your API product and describes the input and output data that is required to execute the service. We use the [OpenAPI Specification v3 (OAS3)](https://swagger.io/specification) to describe the API of a service. ## Endpoints Managed Services expose an API to asynchronously execute the service and retrieve the results. The following table lists the available endpoints: | Method | Path | Description | |:-------|:----------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `GET` | `/` | Retrieves a list of all service executions. The response includes links to each service execution, allowing for further queries on their status and results. | | `POST` | `/` | This method is used to run the service asynchronously while sending the appropriate input. Accepts a JSON object, which will be passed to the service code (e.g., the `run()` method in the `program.py` file). The response contains the ID of the service execution. | | `GET` | `/{id}` | Check the status of a service execution. The status can be one of the following: `PENDING`, `RUNNING`, `SUCCEEDED`, `CANCELLED`, `FAILED`. The response also includes timestamps to provide information about when the execution was created, started, and completed. | | `GET` | `/{id}/result` | Get the result of a service execution. The response contains the JSON result from the service (if any). Further, it provides links to download all result files created during the service execution. | | `GET` | `/{id}/result/{file}` | Download a result file of a service execution. The content type of the HTTP response is set to the content type of the file. If it cannot be determined, the content type is set to `application/octet-stream`. | | `GET` | `/{id}/log` | Get the log output of a service execution line by line. | | `PUT` | `/{id}/cancel` | Cancels a `PENDING` or `RUNNING` service execution. | > \[!IMPORTANT] > Do **NOT** change the operations/endpoints or add new ones, otherwise communicating with the service will not work as intended. ## Describing your API > \[!NOTE] We provide a template!\ > Our default API description, which can be used as a template, can be downloaded: . As a service provider, you can (and should) change titles and descriptions for the different endpoints. Besides that, it is highly recommended to describe the format of the inputs and outputs withing the `components.schemas` section of the API specification. This is especially important for the `POST /` endpoint, since it defines what kind of input data may be provided by the user. Further, the response specification for `GET /{id}/result` endpoint is equally important, since it defines what kind of output the user can expect when successfully running the service. You may use the integrated Swagger Editor (Service Details > API Specification) to edit the API description. Alternatively, use the [Swagger Online Editor](https://editor.swagger.io) or an OpenAPI editor extensions for your IDE, e.g,. for [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi). ### Title and Description We highly recommend to change the title and description of the API to match your service. | Field | Description | |:-------------------|:--------------------------------| | `info.title` | The title of the API. | | `info.description` | A short description of the API. | Example: ```yaml info: title: Service API description: | API description for a managed Service. ``` ### Input Data and Parameters Each managed service retrieves input data and parameters from the user when executed via the `POST /` endpoint. | Field | Description | |:---------------------------------|:------------------------------------| | `components.schemas.InputData` | The schema of the input data. | | `components.schemas.InputParams` | The schema of the input parameters. | If you are using the [Starter template for Python projects](https://dashboard.hub.kipu-quantum.com/community/implementations/1a0ae675-4b23-405c-af8e-f4189ff14e0), the input data and parameters are defined by the signature of the `run()` method in the `program.py` file. For example, if you defined the `run()` method as `def run(data: Dict[str, Any], params: Dict[str, Any]) -> Dict[str, Any]:`, this could imply that users may send the following API request bodies: ```json { "data": { "values": [1, 2, 3] }, "params": { "round_up": true } } ``` In case you are using a [custom Docker Container](custom-containers), the platform ensures that the input provided via the Service API in the form of is mounted into the container at runtime. For example, given the input from above, the runtime creates the following files: * `data.json` with the content `{ "values": [1, 2, 3] }` * `params.json` with the content `{ "round_up": true }` These files are mounted into the directory `/var/runtime/input` of the running container. Either way, the input data and parameters should be described in the API description. For example, the following snippet describes the input data and parameters for a service that sums up a list of numbers and optionally rounds up the result. ```yaml components: schemas: InputData: type: object # # Define the schema of your input data here # properties: values: description: List of values to sum up type: array items: type: number example: [1, 2, 3] InputParams: type: object # # Define the schema of your input parameters here # properties: round_up: description: Whether to round up the result type: boolean example: true ``` In general, input data should encode the information about the actual problem (e.g., the entries of a QUBO-matrix) while input parameters are additional information to influence the evaluation (e.g., the number of ancillary qubits for an execution). Learn more about how to define the schema of your input data and parameters [here](https://swagger.io/specification/#schema-object) or which data types are supported [here](https://swagger.io/specification/#data-types). ### Responses (aka. Output) A service may produce different kinds of output. First, there is the return value of the `run()` method in the `program.py` file. The response type must be a JSON-serializable object, e.g., a dictionary or Pydantic model. This kind of output will be returned in the response of the `GET /{id}/result` endpoint. The respective schema should be defined in the `ResultResponse` section. | Field | Description | |:------------------------------------|:-----------------------------------| | `components.schemas.ResultResponse` | The schema of the result response. | Considering the example from above, the response of the `run()` method could be a dictionary with the sum of the values: `{ "sum": 6 }` This may result in the following schema definition: ```yaml components: schemas: ResultResponse: type: object additionalProperties: type: string properties: # # Add the schema of your result response here # sum: description: The sum of the values type: number example: 6 # DO NOT REMOVE THE FOLLOWING LINES _links: type: object properties: status: $ref: '#/components/schemas/HALLink' additionalProperties: $ref: '#/components/schemas/HALLink' _embedded: type: object properties: status: $ref: '#/components/schemas/ServiceExecution' ``` Learn more about how to define the schema of your input data and parameters [here](https://swagger.io/specification/#schema-object) or which data types are supported [here](https://swagger.io/specification/#data-types). Further, a service may write additional output data to files in the `/var/runtime/output` directory. These files can be downloaded by the user via the `GET /{id}/result/{file}` endpoint. The `ResultResponse` schema already defines a `_links` property that contains the respective download links. If the `run()` method returned a dictionary with the sum of the values, there will also be a file `output.json` containing the same information (`{ "sum": 6 }`). A full API response could look like this: ```json { "sum": 6, "_embedded": { "status": { "id": "ee49be82-593d-4d12-b732-ab84e0b11be1", "createdAt": "2025-03-14 14:09:33", "startedAt": "2025-03-14 14:10:45", "endedAt": "2025-03-14 14:11:00", "status": "SUCCEEDED" } }, "_links": { "self": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1/result" }, "status": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1" }, "hello.jpg": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1/result/hello.jpg" }, "output.json": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1/result/output.json" } } } ``` Last but not least, there is log output of a service. The complete log output can be retrieved line by line via the `GET /{id}/log` endpoint. --- --- url: /services/managed/jobs.md description: >- Execute Managed Services asynchronously as jobs, store their results, and share outcomes with other users via the Job Dashboard. --- # Run as a Job Jobs provide an easy way to execute your Managed Services in an asynchronous manner, store the results, and retrieve them later on. They are especially useful when experimenting with implementations for quantum hardware and when intending to share results with other users. You can create a Job either via [our CLI](../../quickstart) or via the [create job page](https://dashboard.hub.kipu-quantum.com/jobs/new). On the [Job Dashboard](https://dashboard.hub.kipu-quantum.com/jobs) you see an overview of all your created jobs. --- --- url: /services/managed/datapool.md description: >- Attach Data Pools to Managed Services and use the qhub-commons DataPool class to read mounted files at runtime. --- # Using Data Pools in Services This guide explains how to use the `DataPool` feature to work with datasets and file collections within your services. ## What is a Data Pool? A Data Pool is a managed collection of files, similar to a directory or a folder, that can be attached to your service at runtime. It provides a simple and efficient way to access large datasets, pre-trained models, or any other file-based resources without having to include them directly in your service's deployment package. When you use a Data Pool, the platform mounts the specified file collection into your service's runtime environment. The `qhub-commons` library provides a convenient `DataPool` abstraction to interact with these mounted files. ::: tip Data Pool Limits Data Pools are designed to handle large datasets, but there are some limits to keep in mind: * The maximum size of a single file in a Data Pool is 500 MB. * The files are mounted using a blob storage technology, which means performance may vary based on the size and number of files. ::: ## How to Use the `DataPool` Class To use a Data Pool in your service, you simply need to declare a parameter of type `DataPool` in your `run` method. The runtime will automatically detect this and inject a `DataPool` object that corresponds to the mounted file collection. ### The `DataPool` Object The `DataPool` object, found in `qhub.commons.datapool`, provides the following methods to interact with the files in the mounted directory: * `list_files() -> Dict[str, str]`: Returns a dictionary of all files in the Data Pool, where the keys are the file names and the values are their absolute paths. * `open(file_name: str, mode: str = "r")`: Opens a specific file within the Data Pool and returns a file handle, similar to Python's built-in `open()` function. * `path`: A property that returns the absolute path to the mounted Data Pool directory. * `name`: A property that returns the name of the Data Pool (which corresponds to the parameter name in your `run` method). ### Tutorial: Building a Service with a Data Pool Let's walk through an example of a service that reads data from a Data Pool. #### 1. Initialize a New Project If you haven't already, create a new service project. You can use the CLI to set up a new service: ```bash qhubctl init cd [user_code] uv venv source .venv/bin/activate uv sync ``` For the rest of this guide, we assume that you created your service in a directory named `user_code`, with the main code in `user_code/src/`. #### 2. Update the `run` Method In your `program.py`, define a `run` method that accepts a `DataPool` parameter. The name of the parameter (e.g., `my_dataset`) is important, as it will be used to identify the Data Pool in the API call. ```python # user_code/src/program.py from qhub.commons.datapool import DataPool from pydantic import BaseModel class InputData(BaseModel): file_to_read: str def run(data: InputData, my_dataset: DataPool) -> str: """ Reads the content of a specified file from a Data Pool. """ try: # Use the open() method to read a file from the Data Pool with my_dataset.open(data.file_to_read) as f: content = f.read() return content except FileNotFoundError: return f"File '{data.file_to_read}' not found in the Data Pool." ``` In this example, the `run` method expects a Data Pool to be provided for the `my_dataset` parameter. #### 3. Local Testing with Data Pools When developing and testing your service locally, you don't have access to the platform's Data Pool mounting system. However, you can easily simulate this by creating a local directory and passing it to your `run` method. ##### Steps for Local Testing 1. **Create a local directory for your Data Pool.** This directory should be placed inside the `user_code/input` directory. The name of this directory can be anything, but for this example, we'll name it `my_dataset` to match the parameter in the `run` method. 2. **Populate the directory with your test files.** Place any files you need for your test inside this directory (e.g., `user_code/input/my_dataset/hello.txt`). And add the value `Hello` to the `hello.txt` file. 3. **Update the `__main__.py` file.** Modify your main entrypoint to manually create a `DataPool` instance and pass it to the `run` function. You will create the `DataPool` object with a relative path to your local Data Pool directory. 4. **Run your service.** Now you can run your service directly without setting any environment variables. ```bash # Run your service's main entrypoint cd user_code python -m src ``` ##### Example Let's assume your project has the following structure: ``` user_code ├── src/ │ ├── __main__.py │ └── program.py └── input/ ├── data.json └── my_dataset/ └── hello.txt ``` And `user_code/input/data.json` contains: ```json { "file_to_read": "hello.txt" } ``` Update your `user_code/src/__main__.py` to look like this: ```python # user_code/src/__main__.py import json import os from qhub.commons.constants import OUTPUT_DIRECTORY_ENV from qhub.commons.datapool import DataPool from qhub.commons.json import any_to_json from qhub.commons.logging import init_logging from .program import InputData, run init_logging() # This file is executed if you run `python -m src` from the project root. Use this file to test your program locally. # You can read the input data from the `input` directory and map it to the respective parameter of the `run()` function. # Redirect the platform's output directory for local testing directory = "./out" os.makedirs(directory, exist_ok=True) os.environ[OUTPUT_DIRECTORY_ENV] = directory with open(f"./input/data.json") as file: data = InputData.model_validate(json.load(file)) result = run(data, my_dataset=DataPool("./input/my_dataset")) print(any_to_json(result)) ``` The `__main__.py` script now manually creates the `DataPool` object and passes it to your `run` function, simulating the behavior of the platform and allowing you to test your `run` method's logic with local files. Now run the service: ```bash python -m src ``` #### 4. Use Multiple Data Pools If your service needs to work with multiple Data Pools, you can simply add more parameters of type `DataPool` to your `run` method. ```python import os from qhub.commons.datapool import DataPool from pydantic import BaseModel class InputData(BaseModel): file_to_read_from_my_dataset: str file_to_read_from_another_dataset: str def run(data: InputData, my_dataset: DataPool, another_dataset: DataPool, output_datapool: DataPool) -> str: """ Combines the content of the specified files from two different Data Pools in a third Data Pool. """ try: with my_dataset.open(data.file_to_read_from_my_dataset) as f1: content1 = f1.read() with another_dataset.open(data.file_to_read_from_another_dataset) as f2: content2 = f2.read() # You can also write to the output Data Pool if needed concatinated_file = os.path.join(output_datapool.path, "concatenated_output.txt") with open(concatinated_file, "w") as out_file: out_file.write(content1) out_file.write(content2) return f"Content from my_dataset: {content1}\nContent from another_dataset: {content2}" except FileNotFoundError as e: return str(e) ``` For local development, you would create two directories in your `input` folder (e.g., `my_dataset` and `another_dataset`) and pass them as separate parameters in the `__main__.py`: ```python # user_code/src/__main__.py ## see from above... result = run(data, my_dataset=DataPool("./input/my_dataset"), another_dataset=DataPool("./input/another_dataset"), output_datapool=DataPool("./input/output_datapool")) ## see from above... ``` Create the `another_dataset` and `output_datapool` directories in your `input` folder. Then, create the file `input/another_dataset/world.txt` with the content `World`. Before running the service, update the data in `input/data.json` to include the new file names: ```json { "file_to_read_from_my_dataset": "hello.txt", "file_to_read_from_another_dataset": "world.txt" } ``` After running the service, you should see the concatenated output file in the `input/output_datapool` directory. ```bash python -m src ``` #### 5. Configuring the Data Pool in the API Call When you execute this service via the platform API, you need to specify which Data Pool to mount. This is done by providing a special JSON object in the request body. The key of this object must match the `DataPool` parameter name in your `run` method (`my_dataset` in our example). The JSON object has two fields: * `id`: The unique identifier (UUID) of the Data Pool you want to use. * `ref`: A static value that must be `"DATAPOOL"`. Here is an example of a request body for our first version of the service: ```json { "data": { "file_to_read": "hello.txt" }, "my_dataset": { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "ref": "DATAPOOL" } } ``` When the service is executed with this input, the platform will: 1. Identify that the `my_dataset` parameter is a Data Pool reference. 2. Mount the Data Pool with the specified `id`. 3. Instantiate a `DataPool` object pointing to the mounted directory. 4. Inject this `Data Pool` object into the `run` method as the `my_dataset` argument. Your code can then use the `my_dataset` object to interact with the files in the mounted Data Pool. Here is an example of a request body for our second version of the service: ```json { "data": { "file_to_read_from_my_dataset": "hello.txt", "file_to_read_from_another_dataset": "world.txt" }, "my_dataset": { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "ref": "DATAPOOL" }, "another_dataset": { "id": "b1b2c3d4-e5f6-7890-1234-567890abcdef", "ref": "DATAPOOL" }, "output_datapool": { "id": "c1b2c3d4-e5f6-7890-1234-567890abcdef", "ref": "DATAPOOL" } } ``` ## OpenAPI Specification for Data Pools When you generate an OpenAPI specification for a service that uses a `DataPool`, the `qhubctl openapi` library automatically creates the correct schema for the Data Pool parameter. Instead of showing the internal structure of the `DataPool` class, it generates a schema that reflects the expected API input format. For the `my_dataset: DataPool` parameter, the generated OpenAPI schema will look like this: ```yaml my_dataset: type: object properties: id: type: string format: uuid description: UUID of the Data Pool to mount ref: type: string enum: [DATAPOOL] description: Reference type indicating this is a Data Pool required: - id - ref additionalProperties: false ``` This ensures that the API documentation accurately represents how to use the service and provides a clear contract for API clients. ## Data Pool Access Grants By default, the platform checks whether the application creator has direct access to a data pool before mounting it during a service execution. **Data Pool Access Grants** provide an alternative: short-lived, JWT-based tokens that authorize access to a data pool without sharing it permanently with the service owner. This is designed for **cross-organization workflows** where a third-party service needs to read from or write to your data pool during execution, but you don't want to grant the service owner permanent access. ### How Access Grants Work 1. **User A** owns a data pool and an application that is subscribed to a service owned by **User B**. 2. **User A** requests a grant by calling the grant endpoint, specifying their application ID and the required permission level (`VIEW` or `MODIFY`). 3. The platform returns a **signed JWT token** scoped to that specific data pool, application, and tenant. The token expires after 15 minutes by default. 4. **User A** attaches the grant token to the volume mount reference when creating a service execution. The platform validates the token at execution time instead of checking whether the service owner has permanent access to the data pool. ### Creating a Grant To create an access grant, send a `POST` request to the grant endpoint for the target data pool: **Endpoint:** `POST /datapools/{datapoolId}/grants` **Request body:** ```json { "applicationId": "", "permission": "VIEW" } ``` | Field | Type | Required | Description | |:----------------|:-------|:---------|:------------------------------------------------------------| | `applicationId` | UUID | Yes | The ID of your application that will be used for execution. | | `permission` | String | Yes | Access level: `VIEW` (read-only) or `MODIFY` (read-write). | **Response (200 OK):** ```json { "token": "eyJhbGciOiJIUzI1NiJ9..." } ``` ::: tip Authorization Requirements The requesting user must have at least `VIEWER` role on the data pool to create a `VIEW` grant, or `MAINTAINER` role to create a `MODIFY` grant. ::: **Error responses:** | Status | Condition | |:-------|:------------------------------------------------------| | 400 | Invalid permission value (must be `VIEW` or `MODIFY`) | | 403 | Insufficient permission on the data pool | | 404 | Data pool not found | ### Using a Grant in a Service Execution When creating a service execution, include the `grant` field on the Data Pool reference object. Using the example from above, the request body would look like this: ```json { "data": { "file_to_read": "hello.txt" }, "my_dataset": { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "ref": "DATAPOOL", "grant": "eyJhbGciOiJIUzI1NiJ9..." } } ``` **Behavior:** * If a `grant` token is present, the platform validates the token (signature, expiration, claim matching) instead of checking whether the application creator has direct permission on the data pool. * The `writeable` flag on the mount is automatically set based on the grant's permission level: `MODIFY` results in a writeable mount, `VIEW` results in a read-only mount. * If no `grant` is provided, the existing behavior applies: the platform checks whether the application creator has direct access to the data pool. ### Token Validation At execution time, the platform rejects the grant token if: * The token signature is invalid or tampered * The token has expired * The `datapoolId` in the token does not match the volume mount reference * The `applicationId` in the token does not match the application running the execution * The `tenantId` in the token does not match the application creator A rejected token results in a `403 Forbidden` response. ### Grant Token Properties | Property | Value | |:-----------|:------------------------------------------------------------| | Algorithm | HMAC-SHA256 (HS256) | | Expiration | 15 minutes (not configurable at the moment) | | Scoped to | A specific data pool ID, application ID, and tenant ID | | Permission | `VIEW` or `MODIFY`, matching what was requested at creation | | Reusable | Yes — the token can be used multiple times until it expires | ### Example Workflow The following example illustrates a typical cross-organization workflow using access grants: ``` 1. User B owns Service "quantum-optimizer" 2. User A owns Data Pool "experiment-results" 3. User A creates Application "my-quantum-run", subscribed to User B's Service 4. User A requests a grant: POST /datapools/{experiment-results-id}/grants { "applicationId": "{my-quantum-run-id}", "permission": "VIEW" } 5. User A receives: { "token": "eyJ..." } 6. User A creates a service execution, attaching the grant token: POST /service-executions { "data": { ... }, "my_dataset": { "id": "{experiment-results-id}", "ref": "DATAPOOL", "grant": "eyJ..." } } 7. The platform validates the grant and mounts User A's data pool as read-only for User B's Service during execution ``` --- --- url: /services/managed/secrets.md description: >- Use the SecretValue class from qhub-commons to securely inject API tokens and credentials as environment variables into Managed Services. --- # Using Secrets in Services This guide explains how to use the `SecretValue` feature to securely handle sensitive information like API tokens, credentials, and passwords within your services. ## What is a Secret? A Secret is a secure way to pass sensitive information to your service at runtime without exposing it, e.g., to your logs. Unlike regular input parameters that are passed through the Service API request body, secrets are protected by multiple security mechanisms. When you use a Secret, the platform: * Securely injects the secret value as an environment variable into your service's runtime environment * Automatically maps the environment variable to your function parameter based on naming conventions * Provides a `SecretValue` abstraction from the `qhub-commons` library that prevents accidental exposure ::: warning Security Best Practices When working with secrets, always follow these security guidelines: * **Never log or print** the unwrapped secret value. * **Use secrets only once** - the `unwrap()` method can only be called once per secret. * **Never commit secrets** to version control or include them in your code. * **Use environment variables** for local testing, not hardcoded values. ::: ## How to Use the `SecretValue` Class To use a Secret in your service, you declare a parameter of type `SecretValue` in your `run` method. The runtime will automatically detect this, load the secret from the corresponding environment variable, and inject a `SecretValue` object that protects the sensitive data. ### The `SecretValue` Object The `SecretValue` object, found in `qhub.commons.secret`, is a secure container that provides the following features: * `unwrap() -> str`: Returns the actual secret value. **Can only be called once** - subsequent calls raise a `ValueError`. * `is_locked`: A property that returns `True` if the secret has been unwrapped, `False` otherwise. * **Automatic redaction**: String representations always show `[redacted]` or `SecretValue([redacted])` to prevent accidental exposure in logs. * **Environment variable mapping**: Automatically loads from environment variables using the pattern `SECRET_{PARAMETER_NAME}` (uppercase). ### Environment Variable Naming Convention The platform automatically maps secret parameters to environment variables using this convention: | Parameter Name | Environment Variable | |---------------------|----------------------------| | `api_token` | `SECRET_API_TOKEN` | | `ibmToken` | `SECRET_IBM_TOKEN` | | `iqm_token` | `SECRET_IQM_TOKEN` | | `database_password` | `SECRET_DATABASE_PASSWORD` | The parameter name is converted to uppercase, and the `SECRET_` prefix is added automatically. ### Tutorial: Building a Service with Secrets Let's walk through an example of a service that uses secrets to authenticate with an external API. #### 1. Initialize a New Project If you haven't already, create a new service project. You can use the CLI to set up a new service: ```bash qhubctl init cd [user_code] uv venv source .venv/bin/activate uv sync ``` For the rest of this guide, we assume that you created your service in a directory named `user_code`, with the main code in `user_code/src/`. #### 2. Update the `run` Method In your `program.py`, define a `run` method that accepts a `SecretValue` parameter. The name of the parameter (e.g., `api_token`) is important, as it determines which environment variable will be used. ```python # user_code/src/program.py from qhub.commons.secret import SecretValue from pydantic import BaseModel import requests class InputData(BaseModel): endpoint: str def run(data: InputData, api_token: SecretValue) -> dict: """ Makes an authenticated API request using a secret token. """ # Use the token for authentication headers = { "Authorization": f"Bearer {api_token.unwrap()}" # Unwrap the secret value (can only be done once) } try: response = requests.get(data.endpoint, headers=headers) response.raise_for_status() return { "status": "success", "data": response.json() } except requests.RequestException as e: return { "status": "error", "message": str(e) } ``` In this example, the `run` method expects a secret to be provided for the `api_token` parameter. The platform will automatically load this from the `SECRET_API_TOKEN` environment variable. #### 3. Local Testing with Secrets When developing and testing your service locally, you need to provide the secret values through environment variables. There are several ways to do this. ##### Option 1: Set Environment Variables Directly The simplest approach is to set the environment variable before running your service: ```bash # Set the secret environment variable export SECRET_API_TOKEN="your-test-token-here" # Run your service python -m src ``` ##### Option 2: Use a `.env` File For better organization, you can create a `.env` file in your project root: ```bash SECRET_API_TOKEN=your-test-token-here ``` ::: danger Never Commit .env Files Add `.env` to your `.gitignore` file to prevent accidentally committing secrets to version control: ``` # .gitignore .env ``` ::: Then load the environment variables from the file: ```bash # Load environment variables from .env file export $(cat .env | xargs) # Run your service python -m src ``` #### 4. Using Multiple Secrets If your service needs to authenticate with multiple external services, you can declare multiple secret parameters: ```python from qhub.commons.secret import SecretValue from pydantic import BaseModel import requests class InputData(BaseModel): fetch_weather: bool fetch_stocks: bool def run(data: InputData, weather_api_key: SecretValue, stock_api_key: SecretValue) -> dict: """ Fetches data from multiple APIs using different credentials. """ results = {} if data.fetch_weather: weather_token = weather_api_key.unwrap() # Use weather_token for weather API... results["weather"] = {"status": "success"} if data.fetch_stocks: stock_token = stock_api_key.unwrap() # Use stock_token for stock API... results["stocks"] = {"status": "success"} return results ``` For local testing, you would set multiple environment variables: ```bash export SECRET_WEATHER_API_KEY="weather-token" export SECRET_STOCK_API_KEY="stock-token" python -m src ``` #### 5. Combining Secrets with Other Input Types You can combine secrets with regular JSON input and Data Pools in the same service: ```python from typing import Dict, Any from qhub.commons.secret import SecretValue from qhub.commons.datapool import DataPool from pydantic import BaseModel class InputData(BaseModel): model_name: str endpoint: str def run( data: InputData, api_token: SecretValue, models: DataPool ) -> dict: """ Loads a model from a Data Pool and uploads it to an API using secret credentials. """ # Unwrap the secret token = api_token.unwrap() # Load the model from the Data Pool model_path = models.list_files()[data.model_name] with open(model_path, "rb") as f: model_data = f.read() # Upload the model using the authenticated API headers = {"Authorization": f"Bearer {token}"} # ... upload logic ... return {"status": "success", "model": data.model_name} ``` For local testing: ```bash # Set the secret export SECRET_API_TOKEN="your-token" # Run with both secret and data pool (assuming a datapool directory and files at './input/models' python -m src ``` ## OpenAPI Specification for Secrets When you generate an OpenAPI specification for a service that uses a `SecretValue`, the `qhubctl openapi` command automatically creates the correct schema for the secret parameter. For example, given this service: ```python def run(api_token: SecretValue) -> dict: pass ``` The command will generate the schema for `api_token` in the following way: ```yaml schema: type: object properties: $secrets: type: object properties: api_token: type: string ``` --- --- url: /services/managed/custom-containers.md description: >- Package a Managed Service as a custom Docker container to bring your own OS packages, languages, or reproducible builds to Kipu Quantum Hub. --- # Custom Docker Containers We support custom Docker containers to run your service. You may consider using "Docker" as your service runtime in the following scenarios: * You need OS-level packages not included in the Python Template. With Docker, you have complete control over your base operating system and installed packages. * Your application is in a language not yet supported by the platform, like Go or Rust. * You need guaranteed reproducible builds. We release regular updates to our coding templates to improve functionality, security, and performance. While we aim for full backward compatibility, using a Dockerfile is the best way to ensure that your production runtime is always in sync with your local builds. > \[!WARNING] Compliance with our runtime interface is required > You cannot run an arbitrary Docker container. > You must comply with our [runtime interface](runtime-interface#docker). ## Set up a Custom Docker Container Project You can use the CLI to bootstrap a custom Docker Container project. Just run `qhubctl init` and select `Docker Starter` as the type of starter project. Alternatively, you can create a new project manually by following the project layout of the [starter-docker](https://dashboard.hub.kipu-quantum.com/community/implementations/a5b16c2a-cf23-49b0-8d1c-e1444c2816ef) repository. A starting folder structure of your project could look like this: ``` . ├── Dockerfile ├── openapi.yaml ├── input │ └── ... └── src └── ... ``` It is important that there is a file called `Dockerfile` in the root directory of the project. The `Dockerfile` is the file that defines the Docker image that will be built by the platform. Optionally, you can provide a file called `openapi.yaml` in the root directory of the project. This file defines the API of your service ([more information](openapi)). The `input` folder may contain the input data and parameters to test your application locally. The `src` folder contains any source code required to run your application. You may extend this structure depending on your needs. ## Build, Run, and Test your Project Build the Docker container: ```shell docker build -t your-app . ``` You can use the `input` directory to provide respective input files for testing. Remember to provide input files according to your expected JSON input structure. For example, if you expect your users to execute your service with the input `{ "data": { ... } }`, you should test with the file `data.json`. By using the command below, any output written to `/var/runtime/output` will be available in the `out` directory after the container has finished running. For more information on how to deal with input and output data, see our runtime interface documentation for [custom Docker containers](runtime-interface#docker). Run the Docker container: ```shell PROJECT_ROOT=(`pwd`) rm -rf $PROJECT_ROOT/out docker run -it \ --user 1000:1000 \ -v $PROJECT_ROOT/input:/var/runtime/input \ -v $PROJECT_ROOT/out:/var/runtime/output \ your-app ``` > \[!IMPORTANT] Your container runs as a non-root user > On the platform, your container runs as a non-root user (UID `1000`) without root privileges. > The `--user 1000:1000` flag above reproduces this locally so you catch permission issues before deploying. > Ensure your code only writes to locations this user can access (e.g. `/var/runtime/output`, `/tmp`), and that any files or directories your `Dockerfile` creates for runtime writes are owned by, or writable for, UID `1000`. ::: tip Windows Users For GitBash users on Windows, replace ```bash PROJECT_ROOT=(`pwd`) ``` with ```bash PROJECT_ROOT=(/`pwd`) ``` For Windows command-prompt users, you can define the volume mounts using `-v %cd%/input:/var/runtime/input` and `-v %cd%/out:/var/runtime/output`. ::: ## What's next? * Learn how to [describe your Service API using OpenAPI Specification v3.0](openapi). * [Deploy your service](introduction.md#create-a-managed-service) using our CLI or web application. --- --- url: /services/managed/runtime-interface.md description: >- Lifecycle, input handling, and output contract for the Python Template and Docker runtime configurations that power Managed Services. --- # Runtime Interface Kipu Quantum Hub offers an asynchronous interface for executing services. This is because the execution of services might take several hours (e.g., for training variational circuits). Therefore, each Service API has one endpoint for submitting (aka. starting) a service execution and other endpoints to poll for the execution status and the result. By polling we avoid client timeouts when waiting for [long-running operation](http://restalk-patterns.org/long-running-operation-polling.html) results. We support two runtime configurations: (1) `Python Template` for Python projects, e.g., according to the [Starter template for Python projects](https://dashboard.hub.kipu-quantum.com/community/implementations/1a0ae675-4b23-405c-af8e-f4189ff14e0), and (2) `Docker` to build a custom Docker Container that can be run as a one-shot process (see the [starter-docker](https://dashboard.hub.kipu-quantum.com/community/implementations/a5b16c2a-cf23-49b0-8d1c-e1444c2816e) repository as an example). ## Python Template When starting with the platform, we recommend using `Python Template` as your runtime configuration. It is best to use the [CLI](../../quickstart) to create a new project based on our starter template [starter-python](https://dashboard.hub.kipu-quantum.com/community/implementations/1a0ae675-4b23-405c-af8e-f4189ff14e0). When using `qhubctl init`, just select `Python Starter` as the type of starter project. ### Lifecycle The Python Template expects a `src` package in the root directory of your project. The `src` package must contain a `__init__.py` and a `program.py` file, containing the `run()` method: ```python def run(data: Dict[str, Any], params: Dict[str, Any]) -> Dict[str, Any]: pass ``` For each service execution, the runtime creates a new Python process and calls the `run()` method. The Python process terminates after the `run()` method returns a result or raises an exception. Next section explains how the `data` and `params` arguments are used to access input provided by the user through the Service API. ### Input A Kipu Quantum Hub Service expects input through multiple mechanisms, provided by the user through the Service API. The runtime processes this input and passes it as arguments to the `run()` method. #### JSON Input The primary input mechanism is a JSON object provided through the Service API ([see `POST /` endpoint](openapi#endpoints)) in the form of `{ "data": , "params": }`. The runtime uses the top-level properties of the input JSON object and passes them as arguments to the `run()` method. For example, given the following input: ```json { "data": { "values": [1, 2, 3] }, "params": { "round_up": true } } ``` The runtime would be able to pass such an input as arguments to the following `run()` method: ```python def run(data: Dict[str, Any], params: Dict[str, Any]) -> Dict[str, Any]: pass ``` Similarly, the runtime supports the use of Pydantic models to define the input data and parameters: ```python class InputData(BaseModel): values: List[float] class InputParams(BaseModel): round_up: bool def run(data: InputData, params: InputParams) -> Dict[str, Any]: pass ``` #### Special Input Types The runtime supports special input types that are declared as additional parameters in the `run()` method and map to additional input fields in the JSON object: * **Secrets**: For securely passing sensitive information like API tokens and credentials. ```json { "data": ..., "$secrets": { "api_token": "my-secret-token" } } ``` The platform ensures that such sensitive information are only accessible during runtime and protected against accidental exposure. * **Data Pools**: For providing access to large datasets through mounted file systems. ```json { "data": ..., "my_dataset": { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "ref": "DATAPOOL" } } ``` These special input types are declared using type annotations and are automatically provided by the runtime based on the parameter name and type. See the following sections for detailed documentation on each special input type. ##### Secrets Services often require access to sensitive information such as API tokens, credentials, or other confidential data. The platform provides a secure mechanism to pass such secrets to your service through the `SecretValue` type as a special form of input. Unlike regular JSON input parameters, secrets are: * Provided through special environment variables (and not treated as regular JSON input) * Automatically mapped to `run()` method parameters based on naming conventions * Protected against accidental exposure through automatic redaction * Only accessible and persistent during the runtime of a service execution You can declare secret parameters as additional arguments in your `run()` method by using the `SecretValue` type annotation: ```python from qhub.commons.secret import SecretValue def run(data: Dict[str, Any], params: Dict[str, Any], api_token: SecretValue) -> Dict[str, Any]: # Access the secret value token = api_token.unwrap() # Use the token... pass ``` This maps to the following JSON input structure: ```json { "data": ..., "params": ..., "$secrets": { "api_token": "my-secret-token" } } ``` Secrets are fed as special environment variables into the runtime. The parameter name is converted to uppercase with a `SECRET_` prefix: * `api_token` → `SECRET_API_TOKEN` * `ibmToken` → `SECRET_IBM_TOKEN` * `iqm_token` → `SECRET_IQM_TOKEN` You will have access to the secret value during runtime by the `SecretValue` interface. The `SecretValue` type is a secure container that provides several security features to protect sensitive information: * **Single-use unwrapping**: Once `unwrap()` is called, the secret is locked to prevent accidental reuse. * **Automatic redaction**: String representations always return `[redacted]` (via `__str__()`) or `SecretValue([redacted])` (via `__repr__()`) to prevent sensitive data exposure in logs or debugging output. > \[!IMPORTANT] > Never log or print the unwrapped secret value. Always use the `SecretValue` object directly in string representations to ensure automatic redaction. ##### Data Pools Data pools provide direct access to mounted file systems as a special form of input, enabling your service to process large datasets that would be impractical to pass through the JSON-based input mechanism. Unlike regular JSON input parameters, data pools: * Provide access to pre-uploaded files through a mounted file system * Are accessed at runtime via `/var/runtime/datapool/{parameter_name}` * Support efficient processing of large files (models, datasets, etc.) * Include a dedicated API for listing and opening files You can declare data pool parameters as additional arguments in your `run()` method by using the `DataPool` type annotation: ```python from qhub.commons.datapool import DataPool def run(data: Dict[str, Any], params: Dict[str, Any], training_data: DataPool) -> Dict[str, Any]: # List available files files = training_data.list_files() # Open and read a file with training_data.open("model.pkl", "rb") as f: model = pickle.load(f) pass ``` This maps to the following JSON input structure: ```json { "data": ..., "params: ..., "training_data": { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "ref": "DATAPOOL" } } ``` Data pools expose files from `/var/runtime/datapool/{parameter_name}` through a dedicated interface to list and open files securely. For example, if you declare a parameter `training_data: DataPool`, the runtime will mount the corresponding data pool at `/var/runtime/datapool/training_data` and provide access through the `DataPool` interface. ### Output #### Main Result A service may produce output by returning a JSON-serializable object from the `run()` method. The result endpoint of the Service API (`GET /{id}/result`) will return such output in the HTTP response body. We recommend to return a dictionary or a Pydantic model. The platform automatically tries to serialize such return types into the HTTP response of your Service API. For example, if the `run()` method would return a dictionary like `{ "sum": 6 }`, the result endpoint would return the following JSON response: ```json { "sum": 6, "_embedded": { "status": { // omitted for brevity } }, "_links": { "self": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1/result" }, "status": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1" }, "output.json": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1/result/output.json" } } } ``` #### Additional Output (Files) The platform treats any file written to `/var/runtime/output` as output of the service. Additional files written to this directory can later be downloaded through the Service API. Respective links are provided in the Service API response, according to the [HAL specification](https://stateless.group/hal_specification.html) (see example above). For example, if you write a file `result.txt` to `/var/runtime/output`, the result response will contain the following link to download the file: `https:////result/result.txt`. We recommend to only use additional files for large outputs that should be downloaded by the user. #### Log Output You can use logging to inform the user about the progress of the service execution or to provide additional information about the result. You may produce log output, either by printing to stdout or by using an appropriate logging framework. Users can retrieve the log output via the `GET /{id}/log` endpoint of the Service API. > \[!WARNING] DO NOT log sensitive information like passwords, API keys, or any other type of confidential information. ### Build Process The Python Template expects a `requirements.txt` file in the root directory of your project. This file should contain all required Python packages for your project. The runtime installs these packages in a virtual environment when containerizing your project. The runtime also expects a `src` package in the root directory of your project. In addition, there must be a `program.py` file in the `src` package, containing a `run()` method. This method is called by the runtime to execute your service. Your code runs as a non-root user without root privileges. Write only to locations your code can access, such as `/var/runtime/output` and `/tmp`. ## Docker If you want to use a custom Docker Container to power your service, you must select `Docker` as your runtime configuration (Service Details). We recommend using "Docker" only if one of the following reasons apply: * You need OS-level packages not included in the Python Template. With Docker, you have complete control over your base operating system and installed packages. * Your application is in a language not yet supported by the platform, like Go or Rust. * You need guaranteed reproducible builds. We release regular updates to our coding templates to improve functionality, security, and performance. While we aim for full backward compatibility, using a Dockerfile is the best way to ensure that your production runtime is always in sync with your local builds. > \[!NOTE] Examples and Starter Template > A starter template for a custom Docker container project can be found in our [starter-docker](https://dashboard.hub.kipu-quantum.com/community/implementations/a5b16c2a-cf23-49b0-8d1c-e1444c2816e) repository. > Another example, using Node.js, can be found in our [node-service](https://dashboard.hub.kipu-quantum.com/community/implementations/47ecdf4f-7168-4262-b7e0-13e39154cdcd) repository. ### Lifecycle You have to create a Docker container that can be run as a one-shot process. This means the Docker container starts, runs your code once and then exits. You may use exit codes to indicate success (exit code `0`) or failure (exit code `1`) of your code. ### Input The platform ensures that the input provided via the Service API in the form of `{ "data": , "params": }` is mounted into the `/var/runtime/input` directory of the running container. The runtime creates a file for each top-level property of the input JSON object. For example, given the following input: ```json { "data": { "values": [1, 2, 3] }, "params": { "round_up": true } } ``` The runtime creates the following files: * `data.json` with the content `{ "values": [1, 2, 3] }` * `params.json` with the content `{ "round_up": true }` > \[!IMPORTANT] > The input for a service must always be a valid JSON object. #### Secrets Services often require access to sensitive information such as API tokens, credentials, or other confidential data. Secrets must be specified in the `$secrets` property of the input JSON object. ```json { "data": ..., "params": ..., "$secrets": { "api_token": "my-secret-token" } } ``` Secrets are fed as special environment variables into the runtime. The parameter name is converted to uppercase with a `SECRET_` prefix: * `api_token` → `SECRET_API_TOKEN` * `ibmToken` → `SECRET_IBM_TOKEN` * `iqm_token` → `SECRET_IQM_TOKEN` You will only have access to the secret value during runtime of the service execution. > \[!IMPORTANT] > Never log or print the secret value. In Python, you could the `SecretValue` container > (from `qhub-commons`) that provides several security features to protect sensitive information. ##### Data Pools Data pools provide direct access to mounted file systems as a special form of input, enabling your service to process large datasets that would be impractical to pass through the JSON-based input mechanism. You must specify data pools in the respective property of the input JSON object. ```json { "data": ..., "training_data": { "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "ref": "DATAPOOL" } } ``` Files are respectively mounted to `/var/runtime/datapool/{parameter_name}` For example, if you declare a field `training_data`, like above, the runtime will mount the corresponding data pool at `/var/runtime/datapool/training_data`. ### Output The platform treats any file written to `/var/runtime/output` as the output of the service. #### Main Result Output that should be returned as HTTP response of the result endpoint (`GET /{id}/result`) must be written to the file `output.json`. For example, if you write the content `{ "sum": 6 }` to `/var/runtime/output/output.json`, the result endpoint will return the following JSON response: ```json { "sum": 6, "_embedded": { "status": { // omitted for brevity } }, "_links": { "self": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1/result" }, "status": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1" }, "output.json": { "href": "...service endpoint.../ee49be82-593d-4d12-b732-ab84e0b11be1/result/output.json" } } } ``` #### Additional Output (Files) Any other file written to `/var/runtime/output` can later be downloaded by the user. Respective links are provided in the Service API response, according to the [HAL specification](https://stateless.group/hal_specification.html) (see example above). For example, if you write a file `result.txt` to `/var/runtime/output`, the result response will contain the following link to download the file: `https:////result/result.txt`. We recommend writing the main result to `output.json` and only use additional files for large outputs that should be downloaded by the user. #### Log Output You can use logging to inform the user about the progress of the service execution or to provide additional information about the result. You may produce log output, either by printing to stdout or by using an appropriate logging framework. Users can retrieve the log output via the `GET /{id}/log` endpoint of the Service API. > \[!WARNING] DO NOT log sensitive information like passwords, API keys, or any other type of confidential information. ### Runtime User Your container runs as a non-root user with user ID (UID) `1000`. Your code has no root privileges: it cannot write to system paths, bind to privileged ports (below `1024`), or install packages at runtime. Make sure your code only writes to locations the non-root user can access, such as `/var/runtime/output` and `/tmp`. If your `Dockerfile` creates files or directories that your code needs to write to at runtime, ensure they are owned by, or writable for, UID `1000`. ### Build Process The Docker runtime expects a `Dockerfile` in the root directory of your project. This file should contain the instructions to build your Docker container. The runtime builds the Docker container according to the instructions in the `Dockerfile`. Make sure you use `CMD` or `ENTRYPOINT` to run your code in the Docker container. For example, if you have a Python script `program.py` in a Python package `starter` that you want to run, you should add the following line to your `Dockerfile`: ``` CMD ["python", "-m", "starter.program"] ``` --- --- url: /services/applications.md description: >- Create Applications with Access Key ID and Secret Access Key credentials to request bearer tokens and securely call services on Kipu Quantum Hub. --- # Applications [Applications](https://dashboard.hub.kipu-quantum.com/applications) are used to interact with the services on Kipu Quantum Hub. Applications hold all necessary information for a secure communication with the service, i.e., a public and secret key pair to request authorization bearer tokens. Authorization bearer tokens must be sent with any request (using the HTTP header `Authorization`) to a service and can be requested from the platform's token endpoint by providing the client credentials, i.e., the *Access Key ID* and *Secret Access Key* of your application. 1. Go to your application in the [Applications](https://dashboard.hub.kipu-quantum.com/applications) section 2. Copy the cURL command into your clipboard by clicking the "Copy Text" button. The command contains already the Access Key ID and Secret Access Key encoded as Base64 string. 3. Paste and execute the command in your favorite shell. 4. The value of the property `"access_token"` contains the authorization bearer token. Take the following cURL command as an example: ``` curl -k -X POST https://gateway.hub.kipu-quantum.com/token -d "grant_type=client_credentials" -H "Authorization: Basic b2g3cWROZHBCZ0N1OGZ1dV8xMjlORkZBbnNZYTpSaGtVYndhamY4WEh6NktpOXdFZUVhVF9LdGth" ``` The response of the command reveals the authorization bearer token in the `access_token` property: ```json { "access_token": "eyJ4NXQiOiJNell4TW1Ga09HWXdNV0kwWldObU5EY3hOR1l3WW1NNFpUQTNNV0kyTkRBelpHUX...", "scope": "default", "token_type": "Bearer", "expires_in": 3600 } ``` You need to add this token in each HTTP request to the HTTP header `Authorization`. For example, using the response from above, you need to add the following HTTP header field to your HTTP requests: ``` Authorization: Bearer eyJ4NXQiOiJNell4TW1Ga09HWXdNV0kwWldObU5EY3hOR1l3WW1NNFpUQTNNV0kyTkRBelpHUX... ``` Then token has an expiration time, i.e., after it expired you need to obtain a new one with the cURL command above. --- --- url: /services/using-a-service.md description: >- Subscribe to an internally published service or a marketplace service with an Application and obtain credentials to call its HTTP API. --- # Using a Service You can use any service and its underlying HTTP API by subscribing to it with an application. ::: tip NOTE Examples showing how to work with services (e.g., via Jupyter notebooks) can be found in the [Implementations](https://dashboard.hub.kipu-quantum.com/community/implementations) on the KQH. ::: There are two ways to subscribe to a service. One is to subscribe to an internally published service, the other is to subscribe to a service that has been published on the marketplace. ## Subscribe to an Internally Published Service 1. Navigate to the "Services" section from the main navigation and locate your service in the list. 2. Open the service details page and click on the "Publish Service" button and select "Internal" to make the service available within your account or organization. 3. Navigate to "Applications" from the main navigation to access your application overview. 4. Select the application you want to use to subscribe to the service and open its details page. 5. On the application details page, click the "Subscribe Internally" button. 6. A dialog box will appear prompting you to select the internally published service you want to subscribe to. 7. Choose the correct service from the list and click the "Subscribe" button in the dialog to complete the subscription. ## Subscribe to a Service Published on the Kipu Quantum Hub Marketplace 1. Click on "Marketplace" in the top navigation bar or access directly. 2. Select "Browse all services" or directly click on one of the services offered by Kipu. 3. Browse through the service listings and select a service you want to subscribe to, then click on it to open its details page. 4. On the service details page, under "Pricing & Subscription" click the "Subscribe" button. This will open a subscription dialog. 5. In the dialog, select the application you want to use for this subscription from the dropdown menu and click the "Subscribe" button to confirm. > Note: You will only be able to subscribe to services with a "Free" or "Commercial" pricing plan. > If a service is offered as "On Request", please reach out to the service provider for further assistance. ## Obtain Service Gateway Credentials After subscribing to a service with your application, you need to obtain the necessary credentials to interact with the service: 1. Navigate to "Applications" and select the application that subscribed to the service. 2. On the application details page, you will find the "Service Gateway Credentials" section containing: * **Token Endpoint**: The URL to request access tokens * **Access Key ID**: Your unique identifier for authentication * **Secret Access Key**: Your secret key for authentication (masked by default) ## Request an Access Token To authenticate your requests to the service, you need to obtain an access token using your Service Gateway Credentials: 1. On your application details page, locate the cURL command provided in the "Service Gateway Credentials" section. 2. The command is pre-configured with your Access Key ID and Secret Access Key encoded in Base64 format. 3. Copy the cURL command by clicking the copy icon next to it. 4. Execute the command in your terminal to request an access token: ```bash curl -k -X POST https://gateway.hub.kipu-quantum.com/token -d "grant_type=client_credentials" -H "Authorization: Basic Base64(access-key-id:secret-access-key)" ``` The response will contain your access token: ```json { "access_token": "eyJ4NXQiOiJNell4TW1Ga09HWXdNV0kwWldObU5EY3hOR1l3WW1NNFpUQTNNV0kyTkRBelpHUX...", "scope": "default", "token_type": "Bearer", "expires_in": 3600 } ``` The access token is valid for the time specified in `expires_in` (in seconds). After expiration, you need to request a new token using the same cURL command. ## Execute a Subscribed Service To execute a service, you need to provide the following information: 1. **Service Endpoint URL**: Can be obtained from the subscriptions section of your application that subscribed to the service 2. **Authorization Bearer Token**: Obtained from the token endpoint using your Service Gateway Credentials as described above 3. **Header Fields**: `Content-Type` and `Accept` are both set to `application/json` 4. **Provide input data** according to the service's OpenAPI specification. The following examples shows the cURL command and how to start a service execution: ``` curl -X 'POST' \ 'https://gateway.hub.kipu-quantum.com/70b6e720-dcec-4b9b-a462-a6fdaf400bfa/myservice/1.0.0/' \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer eyJ4NXQiOiJNell4TW1Ga09HWXdNV0kwWldObU5EY3hOR1l3WW1NNFpUQTNNV0kyTkRBelpHUXp...' \ -d '{ "data": { "values": [ 100, 50, ] }, "params": { "round_off": false } }' ``` For each call, a new service execution is created. The id of the service execution and its initial execution state is returned by the POST request. The POST above returns for instance the following result: ```json { "id": "02e0d85a-5a95-4abe-a642-1ee9a94fdf14", "status": "PENDING", "createdAt": "2022-09-19 16:45:24" } ``` The execution id can be used to query the state and the result of a service execution. ## Retrieve Execution Result Since a service execution is performed asynchronously, you need to query its state to know if its execution completed. After a service was completed you can retrieve the result. Therefore, a service provides dedicated endpoints to query the state of each of its executions. The state can be retrieved by calling the endpoint `GET /{id}`. The state of our example service execution with id `02e0d85a-5a95-4abe-a642-1ee9a94fdf14` can be for instance retrieved with the following request: ``` curl -X 'GET' \ 'https://gateway.hub.kipu-quantum.com/70b6e720-dcec-4b9b-a462-a6fdaf400bfa/myservice/1.0.0/02e0d85a-5a95-4abe-a642-1ee9a94fdf14' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ...' ``` If the execution is still running, you should get `"status": "RUNNING"` or `"status": "PENDING"`. When the execution finished successfully, you should see `"status": "SUCCEEDED"`. If you get either `"status": "FAILED"` or `"status": "UNKNOWN"` something went wrong. You can retrieve the result or the cause of an error through the endpoint `GET /{id}/result`: ``` curl -X 'GET' \ 'https://gateway.hub.kipu-quantum.com/70b6e720-dcec-4b9b-a462-a6fdaf400bfa/myservice/1.0.0/02e0d85a-5a95-4abe-a642-1ee9a94fdf14/result' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer ...' ``` --- --- url: /services/sharing-services.md description: >- Share services privately with specific users or organizations, with optional time-based constraints, without publishing to the marketplace. --- # Sharing Services The service sharing feature allows you to grant access to your services with other users or organizations without publishing them to the marketplace. This enables private collaboration and controlled access to your quantum services. ::: tip Use Cases * Share development versions with team members * Provide temporary access to external collaborators * Grant time-limited access for evaluation purposes * Collaborate across organizational boundaries without public exposure ::: ## Overview Service sharing provides a flexible way to control who can access and use your services: * **Direct Sharing**: Share services immediately with specific users or organizations * **Constraint-Based Sharing**: Add time-based or other constraints to control when and how long a share is active * **Private Collaboration**: Enable service usage without marketplace publication * **Granular Control**: Manage each share independently with specific constraints ## Creating a Share You can create shares from the service details page. A share becomes active as soon as it's created and remains active until you remove it or add/change constraints or until any defined constraints expire. **To create an immediate share:** 1. Navigate to your service details page 2. Click on the "Sharing" tab 3. Select "Add Share" 4. Choose the recipient type: * **User**: You can search for a specific user using their name, email, or user ID * **Organization**: You can search for an organization by name or ID 5. Click "Create Share" to activate immediately The shared service will appear in the recipient's service list and can be used immediately without any marketplace subscription. ::: warning Important If you only want users to test the service temporarily via the "Service Jobs" interface, you can create a share without publishing it internally. If you want users to be able to integrate the service into applications, you must publish it internally for the API to become available for applications. ::: ## Adding Constraints to Shares Constraint-based sharing allows you to add conditions that control when and for how long the share is active. **To create a share with constraints:** 1. Add a new share as described above 2. After creating a share, you can edit it to add constraints by clicking on "Add Constraint": ::: warning Important For a share to be active, all constraints must be satisfied. For example, if you set a start date in the future, the share will not be active until that date is reached. If you set multiple constraints, the share will only be active when all constraints are met, i.e., without overlapping time periods, a share may never become active! ::: ### Time-Based Constraints Time-based constraints allow you to define a specific period during which the share is active. | Constraint | Description | Example Use Case | |----------------|-------------------------------------------------|--------------------------------------------| | **Start Date** | The date and time when the share becomes active | Schedule access to begin at a future date | | **End Date** | The date and time when the share expires | Grant temporary access for a trial period | ::: tip Best Practices * Set end dates for evaluation or trial access * Use start dates to schedule access in advance * Combine both for time-boxed collaborations * Review and update constraints as project needs change ::: ::: info Limitation Currently, we only support time-based constraints. Future updates may include additional constraint types. ::: ## Managing Shares ### View Active Shares You can view all active shares for your service from the service details page: 1. Navigate to your service details page 2. Click on "Shares" or "Manage Shares" 3. View the list of all active shares with their: * Recipient (user or organization) * Creation date * Active constraints * Status (active, pending, expired) ### Edit Share Constraints You can modify constraints for existing shares: 1. Locate the share in your shares list 2. Click on "Edit" or the settings icon 3. Modify the constraints: * Update start/end dates * Add new constraints * Remove existing constraints 4. Save your changes The updated constraints will take effect immediately. ### Revoke a Share To remove access for a user or organization: 1. Locate the share in your shares list 2. Click on "Remove" or the delete icon 3. Confirm the removal ::: warning Important Revoking a share immediately removes access to the service. ::: ## Share Permissions When you share a service, you can set the permission of recipients to Viewer or Maintainer., A viewer can: * View the service details and documentation * Subscribe to the service with their applications * Execute the service through the API * View their execution history In addition, Maintainers can: * Modify the service code or configuration * Publish the service to the marketplace ## Using a Shared Service If someone has shared a service with you: 1. Navigate to "Services" 2. The shared service will appear in your service list 3. Subscribe to it with your application (same process as internal services) 4. Use the service through the standard [service execution workflow](./using-a-service.md) ## Best Practices ### Security Considerations * **Review shares regularly**: Periodically audit active shares to ensure they're still needed * **Use time constraints**: Always set end dates for temporary collaborations * **Principle of least privilege**: Only share with users/organizations that need access * **Monitor usage**: Check service execution logs for shared services ### Collaboration Workflows * **Development Sharing**: Share development versions with team members for testing * **External Review**: Provide time-limited access to external reviewers or evaluators * **Partner Access**: Share production services with trusted partners without marketplace exposure * **Temporary Projects**: Use date ranges for project-based collaborations ### Constraint Management * **Buffer Time**: Set end dates with some buffer for users to complete their work * **Advance Scheduling**: Use start dates to prepare access ahead of collaboration start * **Regular Review**: Check pending and expired shares monthly * **Documentation**: Keep notes on why each share was created and its purpose ## Differences from Marketplace Publishing | Feature | Service Sharing | Marketplace Publishing | |----------------------|------------------------|---------------------------| | **Visibility** | Only shared recipients | All platform users | | **Access Control** | Granular per user/org | Public with pricing plans | | **Constraints** | Time-based and custom | Based on pricing plans | | **Revenue** | No billing/payments | Monetization possible | | **Use Case** | Private collaboration | Public distribution | | **Approval Process** | Immediate | May require review | ::: tip When to Use Sharing vs. Publishing * **Use Sharing** for private collaborations, team access, temporary evaluations, and controlled distribution * **Use Publishing** for public services, monetization, broad availability, and community contributions ::: ## Frequently Asked Questions **Q: Can recipients share the service with others?** A: No, shares are not transitive. Only the original service owner can create shares. **Q: What happens to running executions when a share expires?** A: Running executions continue to completion, but new executions cannot be started after expiration. **Q: Can I share the same service with multiple organizations?** A: Yes, you can create separate shares for each organization with different constraints. **Q: Do shared services count against the recipient's quotas?** A: Yes, service executions count against the recipient's execution quotas, not the service owner's. ## Related Documentation * [Using a Service](./using-a-service.md) - How to execute shared services * [Publishing to Marketplace](./on-premise/publish-marketplace.md) - Public service distribution * [Service Configuration](./managed/service-configuration.md) - Service setup and configuration * [Manage Organizations](../manage-organizations.md) - Organization-level sharing --- --- url: /services/workflow/air-traffic-tutorial.md description: >- End-to-end BPMN workflow tutorial solving an air traffic graph-coloring problem by orchestrating quantum services on Kipu Quantum Hub. --- # Building an Air Traffic Management Workflow with Quantum Computing ## Overview Welcome to this comprehensive tutorial on creating BPMN workflows for the Platform platform! You'll learn workflow fundamentals while building a complete quantum application that solves a real-world optimization problem: **air traffic management**. By the end of this tutorial, you'll be able to: * Understand BPMN basics and workflow concepts * Use the visual workflow modeler effectively * Design both sequential and parallel service execution * Implement proper data flow between services * Create production-ready quantum workflows * Test and debug complex workflows ### The Problem We're Solving When multiple flight routes intersect, a flight authority must assign each flight to different air corridors to prevent collisions. This is a complex optimization problem that becomes computationally expensive as the number of flights increases. Mathematically, we can represent this problem as a so-called graph-coloring problem, where each flight route is a node and intersecting routes are connected by edges. This is a well-known NP-hard problem in computer science. We can leverage quantum computing to optimize air corridor assignments efficiently. For a more detailed explanation of the underlying optimization problem, see our [Air Traffic Management Use Case](https://dashboard.hub.kipu-quantum.com/use-cases/bd58ec9f-42ef-4ec7-bd79-86afb85dad97). We use the following running example throughout the tutorial: Consider the following five flight routes between European cities: * HEL → FCO (Helsinki to Rome) * BER → MAD (Berlin to Madrid) * CDG → OTP (Paris to Bucharest) * FCO → CDG (Rome to Paris) * OTP → OSL (Bucharest to Oslo) On a map, these routes look like this: ![Flight Route Visualization](./problem-visualization.jpeg "Map showing five flight routes between European cities, used as input for the air traffic optimization workflow.") Given these flight routes, we have several intersections: * HEL → FCO intersects with * OTP → OSL * CDG → OTP * BER → MAD intersects with * FCO → CDG * CDG → OTP Thus, our air traffic management system must: * Identify which routes intersect * Use quantum optimization to find non-conflicting corridors for intersecting routes * Generate visual maps showing the corridor assignments ### What You'll Build An air traffic management system that: 1. **Encodes** flight routes into a quantum optimization problem 2. **Solves** the problem using quantum computing (Illay Base Quantum Optimizer service) 3. **Decodes** the quantum solution back to flight corridor assignments 4. **Visualizes** both the problem and solution on generated maps 5. **Runs in parallel** to generate the problem visualization while solving the quantum problem ### Why Use Workflows? Workflows solve a key challenge in quantum computing: orchestrating complex, multi-step processes without manual programming. Instead of writing Python code to integrate individual Platform services, you can: * ✅ **Visually design** your process flow using BPMN diagrams * ✅ **Automate service execution** with built-in error handling * ✅ **Handle long-running processes** (hours to weeks) reliably * ✅ **Monitor progress** in real-time * ✅ **Reuse workflows** as standalone Platform services * ✅ **Bridge technical and business requirements** for better collaboration ## Part 0: Understanding BPMN Basics Before diving into workflow creation, let's understand the fundamentals of [BPMN](https://bpmn.org/) (Business Process Model and Notation). ### What is BPMN? BPMN is a standardized visual language for modeling business processes. Within the platform, we use BPMN 2.0 to define how quantum services should be executed and how data flows between them. ### Key BPMN Elements for Platform Workflows | Element | Symbol | Icon | Purpose | |---------------------------|--------|--------------------------------------------------------------------------------------------------------------------|----------------------------------------| | **Start Event** | ○ | | Marks where your workflow begins | | **End Event** | ● | | Marks where your workflow ends | | **Platform Service Task** | ▢ | | Executes a subscribed Platform service | | **Parallel Gateway** | ◇+ | | Splits flow to run tasks in parallel | | **Sequence Flow** | → | → | Shows the order of execution | ### Example: Simple Sequential Workflow ``` ○ → [Generate Circuit] → [Execute on Backend] → [Send Results] → ● ``` This workflow executes three services one after another in sequence. ### Example: Parallel Execution Workflow ``` ○ → [Generate Circuit] → ◇+ → [Backend 1] → ◇+ → [Send Results] → ● └ → [Backend 2] → ┘ ``` This workflow generates a circuit once, then executes it on two different backends simultaneously. The parallel gateway (◇+) splits the flow, and another parallel gateway synchronizes the results. ### Understanding the Workflow Modeler Interface When you create a workflow service on our platform, you'll work with the visual workflow modeler. Here are its main components: ![Workflow Modeler Interface](./workflow_modeler_interface.png) #### The Canvas (Center) * **Central workspace** where you design your workflow * Initially shows only a **Start Event** (○) * **Drag and drop** elements from the palette to build your workflow * **Click and drag arrows** to connect elements #### The Palette (Left Side) * Contains all BPMN elements you can use * **Common elements** are visible by default: * Platform Service Task (▢) * Parallel Gateway (◇+) * Exclusive Gateway (◇×) * End Event (●) * Click **"..."** for advanced elements (loops, conditional flows, boundary events) #### The Properties Panel (Right Side) * Displays configuration options for the selected element * **Click on any element** to see its properties here * Use it to: * Configure service subscriptions * Set input/output data mappings * Define API parameters * Add error handling * Set timeouts ### How Data Flows in Workflows: Workflow Variables and Data Mapping Each service in your workflow can: * **Receive input data** from previous steps (or from the workflow input) * **Produce output data** for following steps * **Access workflow variables** defined anywhere in the workflow You can use variables to store and pass data between services. This includes input parameters, service outputs, and intermediate results. All variables are stored in a shared context accessible by all workflow tasks. As a result, you can define an input parameter at the Start Event and use it in any subsequent service. Similarly, outputs from one service can be stored as variables and used later. Variables are defined in two ways: 1. *At the Start Event, i.e., as input parameters*: Define input parameters in the "API Description" section of the Start Event. This enables the modeler to recognize the parameters as workflow variables. For example, consider you want to have two input parameters and define the following at the API description of the Start Event: ```json { "flightRoutes": [ { "origin": "HEL", "destination": "FCO" } ], "mapOutput": { "ref": "datapool", "id": "uuid-of-your-datapool" } } ``` Within the workflow, these inputs become available as variables with name `flightRoutes` and `mapOutput` that can be used throughout the workflow. 2. *From Service Tasks:* Store service outputs into output variables. Our modeler supports the API description of a service. Thus, you can see what outputs are available to store as variables. You can even select a nested object from a service's output to store as a variable. For example, consider a service with output like this: ```json { "result": { "id": "12345", "status": "completed" }, "execution_time": 120 } ``` Then you can store the entire `result` object as a variable or just the `id` field by specifying `result.id`. We'll see concrete examples of this later in the tutorial. For more information, see our [Data Manipulation in Workflows](./data-manipulation.md) guide. ::: warning Heads-up You won't see the **Workflow** tab until **Part 2** when you create the Workflow Service. ::: ## Part 1: Understanding the Workflow Architecture ### Workflow Overview ``` ○ → [Encode Routes] → ◇+ → [Solve with Illay] → [Decode Solution] → ◇+ → [Visualize Solution] → ● └ → [Visualize Problem] -------------------→ ┘ ``` ### Services #### 1. AirSpace Encoder The AirSpace Encoder service converts flight routes into a quantum optimization problem. It analyzes which routes intersect and creates a quantum problem where the goal is to minimize conflicts by assigning routes to different corridors. ##### Input ```json { "flight_routes": [ { "origin": "HEL", "destination": "FCO" }, { "origin": "BER", "destination": "MAD" } ] } ``` ##### Output * `coefficients`: Mathematical representation of the optimization problem * `airports`: List of airports with coordinates (for visualization) * `route_mapping`: Mapping between flight routes and quantum qubits #### 2. Illay Base Quantum Optimizer Service The Kipu Illay Base Quantum Optimizer service solves a quantum optimization problem. In this case, the one encoded by the AirSpace Encoder. To achieve that, it executes a quantum algorithm to find the optimal corridor assignments that minimize conflicts. ##### Input * `problem`: The problem from the encoder * `problem_type`: "binary" (each route is assigned yes/no to each corridor) * `shots`: Number of quantum circuit executions (higher = more accurate) * `num_greedy_passes`: Optimization iterations ##### Output * `result`: Quantum solution * `result.mapped_solution`: Best solution found #### 3. AirSpace Decoder The AirSpace Decoder converts the quantum solution back to human-readable format. ##### Input * `solution`: The quantum result from Illay * `routes`: The quantum mapping from the encoder ##### Output * `channels`: List of corridors with their assigned routes. For example: ```json [ { "channel": "Corridor 0", "routes": [ { "origin": "HEL", "destination": "FCO" } ] }, { "channel": "Corridor 1", "routes": [ { "origin": "CDG", "destination": "OTP" }, { "origin": "FCO", "destination": "CDG" } ] } ] ``` #### 4. Air Traffic Visualizer The Air Traffic Visualizer creates visual maps of flight routes and corridor assignments. ##### Input * `channels`: Corridor assignments from decoder (or custom structure for problem visualization) * `airports`: Airport coordinates from encoder * `file_output_dir`: Datapool location to store the image * `filename`: Name for the generated visualization ##### Output Saves an interactive map visualization to the specified datapool ## Part 2: Creating the Workflow Service ### Prerequisites Before starting this tutorial, you should: * Be familiar with basic quantum computing concepts * Have a new [application](https://dashboard.hub.kipu-quantum.com/applications) created for our new workflow service * **Name your application** anything you prefer (e.g., `air-traffic-demo`). Consistent naming helps when selecting services later. * Have subscriptions to the following Kipu services within the application: * [Illay Base Quantum Optimizer](https://hub.kipu-quantum.com/marketplace/services/4e7919d4-7509-455c-ba28-3614fad695ff) You can create a free subscription to this service using our marketplace. Simply navigate to "Pricing & Subscription" and click "Subscribe". **Subscribe to Illay within the same application** you'll use for this workflow **before** opening the Workflow Modeler. * Create three new services from our public implementation: 1. Click each implementation link below: * [AirSpace Encoder](https://dashboard.hub.kipu-quantum.com/community/implementations/74b1be35-b5b5-4b92-bf11-d2466650fe1e) - Converts flight routes to quantum problems * [AirSpace Decoder](https://dashboard.hub.kipu-quantum.com/community/implementations/0ca68e6d-06aa-4d24-86ec-e30922ca7faa) - Converts quantum solutions to corridor assignments * [Air Traffic Visualizer](https://dashboard.hub.kipu-quantum.com/community/implementations/07edb294-374b-441c-9adf-9565c661b9a0) - Generates map visualizations 2. **Click "Create Service" first** on the top right inside each implementation, then return here for the next steps * **Publish internally:** Open each service → **Publish** → \*\*Internal \*\*. You should see them on the top of your [list of services](https://dashboard.hub.kipu-quantum.com/services). * Subscribe to each AirSpace service within your application: 1. Navigate to your [application's subscriptions page](https://dashboard.hub.kipu-quantum.com/applications) 2. Click "Subscribe Internally" 3. In the dropdown, select each AirSpace service and click "Subscribe" ::: warning Important Make sure to subscribe to all the services within an application. \*\*Use Personal context \*\* for this tutorial; other contexts (e.g., "Kipu Quantum") may cause errors. ::: ::: tip Use the same application for all services. You can subscribe to multiple services within one application as well as have subscriptions to the same service in different applications. ::: ### Naming Conventions Used in This Tutorial Throughout this tutorial, we use consistent names for each workflow step. Here's a quick reference: | Logical step | Standard name | Notes | |----------------------|----------------------------------|--------------------------------------------------| | Encoder service task | **Encode Flight Routes** | Converts flight routes to quantum problems | | Illay optimizer task | **Solve Quantum Problem** | Executes quantum optimization | | Decoder service task | **Decode Quantum Solution** | Converts quantum results to corridor assignments | | Problem visualizer | **Visualize Original Problem** | Shows the input problem visualization | | Solution visualizer | **Visualize Optimized Solution** | Shows the optimized solution visualization | ### Step 1: Create the Workflow Service 1. Navigate to [service creation](https://dashboard.hub.kipu-quantum.com/services/new) 2. Select **"Quantum Workflow Service"** 3. Fill in the details: | Field | Value | |-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Name | Air Traffic Management Workflow | | Summary | Quantum-optimized air corridor assignment for flight routes | | Description | This workflow uses quantum computing to assign flight routes to air corridors, minimizing the risk of collisions when routes intersect. It encodes flight routes as a quantum optimization problem, solves it using Illay, and generates visual maps showing both the problem and solution. | 4. Click "Create Service" ### Step 2: Open the Workflow Modeler 1. Click on your newly created service 2. Navigate to the "Workflow" tab 3. You'll see the workflow modeler with a single Start Event (○) ## Part 3: Building the Workflow Control Flow :::warning You can only use services in your workflow to which you have a valid subscription. Thus, if you developed a service yourself, you must publish it internally and add a subscription to it in an application. Similarly, for public services, you must have a valid subscription within an application. ::: We will model a workflow that should look something like this in the end: ![Completed Air Traffic Workflow](./tutorial-final-workflow.png) ### Step 1: Define Workflow Input First, configure what data your workflow will accept: 1. Select the Start Event or create one (○) 2. In the **properties panel** (right side), navigate to **"API Description"** 3. Add this example request: ```json { "flightRoutes": [ { "origin": "HEL", "destination": "FCO" }, { "origin": "BER", "destination": "MAD" }, { "origin": "CDG", "destination": "OTP" }, { "origin": "FCO", "destination": "CDG" }, { "origin": "OTP", "destination": "OSL" } ], "mapOutput": { "ref": "datapool", "id": "uuid-of-your-datapool" } } ``` ::: tip Understanding the Input * `flightRoutes`: Array of flight routes to optimize (use [IATA airport codes](https://en.wikipedia.org/wiki/IATA_airport_code)) * `mapOutput`: Datapool reference where visualization images will be saved * `ref`: Always "datapool" for platform datapools * `id`: Your datapool UUID **Find your Datapool UUID:** Navigate to **Data → Datapools** in the left navigation, open your datapool, and copy the \*\*UUID \*\* from the details panel. ::: ### Step 2: Add the AirSpace Encoder 1. **Drag** a **Platform Service Task** from the palette onto the canvas 2. **Connect** the Start Event to this task (click Start Event, drag the arrow) 3. Click the **wrench icon** (🔧) on the task 4. **Select** the **AirSpace Encoder** service from the dropdown 5. In the **General** tab (properties panel), set **Name** to: `Encode Flight Routes` ### Step 3: Create Parallel Gateway for Dual Visualization After encoding, we want to: * Solve the quantum problem **AND** * Visualize the original problem Both can happen in parallel: 1. **Drag** a **Gateway** (◇) after the encoder task 2. Click on the Gateway and make it a **Parallel Gateway (◇+)** by clicking on the wrench icon 3. **Connect** the encoder task → parallel gateway 4. This gateway will split the flow into two parallel paths ### Step 4: Add Quantum Solving Branch (Top Path) #### 4.1: Add Illay Base Quantum Optimizer Service 1. **Drag** a **Platform Service Task** above the parallel gateway 2. **Connect** parallel gateway → this task 3. Click **wrench icon** (🔧), select **Illay Base Quantum Optimizer Service** 4. Set **Name** to: `Solve Quantum Problem` #### 4.2: Add Decoder Service 1. **Drag** another **Platform Service Task** after Illay 2. **Connect** Illay task → this task 3. Click **wrench icon** (🔧), select **AirSpace Decoder** 4. Set **Name** to: `Decode Quantum Solution` ### Step 5: Add Problem Visualization Branch (Bottom Path) 1. **Drag** a **Platform Service Task** below the parallel gateway 2. **Connect** parallel gateway → this task (second branch) 3. Click **wrench icon** (🔧), select **Air Traffic Visualizer** 4. Set **Name** to: `Visualize Original Problem` ### Step 6: Add Synchronization Gateway Both parallel branches must complete before continuing: 1. **Drag** another **Parallel Gateway** to the right 2. **Connect** the decoder task → this synchronization gateway 3. **Connect** the problem visualizer task → this synchronization gateway ::: tip Gateway Default When you drag a Gateway from the palette, the default is **Exclusive**. For this tutorial, switch it to \*\*Parallel (◇+) \*\* by clicking the gateway and selecting the parallel type from the properties panel. ::: ### Step 7: Add Solution Visualization 1. **Drag** a **Platform Service Task** after the synchronization gateway 2. **Connect** synchronization gateway → this task 3. Click **wrench icon** (🔧), select **Air Traffic Visualizer** 4. Set **Name** to: `Visualize Optimized Solution` ### Step 8: Add End Event 1. **Drag** an **End Event** (●) from the palette 2. **Connect** the solution visualizer → end event 3. **Save** your workflow (button in top left) ### Verify Your Control Flow Your workflow should now look like this: ``` ○ → [Encode Flight Routes] → ◇+ → [Solve Quantum Problem] → [Decode Quantum Solution] → ◇+ → [Visualize Optimized Solution] → ● └ -→ [Visualize Original Problem] -----------------------→ ┘ ``` Your workflow should look similar to the image above. :::tip Make sure to have the green "No issues" indicator at the bottom of the modeler. If it is grey, simply click on it to activate the model errors. If there are any issues, you will see a red indicators at the corresponding elements. ::: ::: warning Publishing Note You **do not need to publish** the newly created \*\*Workflow Service \*\* itself for this tutorial. Deploying the workflow (covered in Part 5) is sufficient for testing. ::: ## Part 4: Configuring Data Flow Now that the control flow is complete, configure how data moves between services. ### Step 1: Configure AirSpace Encoder **Select** the "Encode Flight Routes" platform service task. #### Inputs Section: Click **+** to add input mapping: | Local variable name | Variable assignment value | Description | |---------------------|---------------------------|--------------------------------------------| | `flight_routes` | `flightRoutes` | Array of flight routes from workflow input | ::: tip Why no quotes? `flightRoutes` (without quotes) references the workflow variable from the Start Event. This directly passes the flight routes array to the encoder service. ::: #### Outputs Section: Store the encoder's three outputs as workflow variables. ::: warning Order Matters In the UI, **Process variable** appears first and **Assignment/Result** second. Follow this order for **all** output mappings. ::: Click **+** for each output mapping: | Process variable name | Result variable name | Description | |-----------------------|----------------------|---------------------------------------------------------| | `coefficients` | `quantumProblem` | Mathematical representation of the optimization problem | | `airports` | `airportsInRoutes` | List of airports with coordinates for visualization | | `route_mapping` | `quantumMapping` | Mapping between flight routes and quantum qubits | ### Step 2: Configure Illay Base Quantum Optimizer Service **Select** the "Solve Quantum Problem" platform service task. #### Inputs Section: Click **+** for each input mapping: | Local variable name | Variable assignment value | Description | |---------------------|---------------------------|-----------------------------------------------------------| | `problem` | `quantumProblem` | The encoded quantum optimization problem from the encoder | | `problem_type` | `"binary"` | Type of optimization problem (quoted literal string) | | `shots` | `1000` | Number of quantum circuit executions for accuracy | | `num_greedy_passes` | `0` | Number of classical optimization iterations | ::: warning Using Literal Values When using literal strings or numbers: * Strings: Use quotes: `"binary"` * Numbers: No quotes: `1000` * Booleans: No quotes: `true` or `false` ::: #### Outputs Section: Click **+** to add output mapping: | Process variable name | Result variable name | Description | |--------------------------|----------------------|----------------------------------------------| | `result.mapped_solution` | `quantumSolution` | Best quantum solution found by the optimizer | ### Step 3: Configure AirSpace Decoder **Select** the "Decode Quantum Solution" platform service task. #### Inputs Section: Click **+** for each input mapping: | Local variable name | Variable assignment value | Description | |---------------------|---------------------------|-----------------------------------------------------| | `solution` | `quantumSolution` | The quantum solution from Illay optimizer | | `routes` | `quantumMapping.routes` | Route mapping from the encoder (using dot notation) | ::: tip Accessing Nested Data `quantumMapping.routes` uses dot notation to access the `routes` property within the `quantumMapping` object. This is standard FEEL expression syntax. ::: #### Outputs Section: Click **+** to add output mapping: | Process variable name | Result variable name | Description | |-----------------------|----------------------|-------------------------------------------------------| | `channels` | `solutionChannels` | Decoded corridor assignments with routes per corridor | ### Step 4: Configure Problem Visualizer **Select** the "Visualize Original Problem" platform service task. #### Inputs Section: Click **+** for each input mapping: | Local variable name | Variable assignment value | Description | |---------------------|----------------------------------------------------|------------------------------------------------------------| | `channels` | `[{"channel": "Problem", "routes": flightRoutes}]` | Single channel containing all original routes (JSON array) | | `airports` | `airportsInRoutes` | Airport coordinates from encoder | | `file_output_dir` | `{"id": mapOutput.id, "ref": "datapool"}` | Datapool reference for saving the visualization | | `filename` | `"problem-visualization"` | Name for the generated problem map image | ::: details Understanding the `channels` Input The visualizer expects an array of channel objects. For the problem visualization, we create a single channel called "Problem" containing all the original routes: ```json [ { "channel": "Problem", "routes": flightRoutes } ] ``` This shows all routes in one color before optimization, helping visualize which routes intersect. ::: ### Step 5: Configure Solution Visualizer **Select** the "Visualize Optimized Solution" platform service task. #### Inputs Section: Click **+** for each input mapping: | Local variable name | Variable assignment value | Description | |---------------------|-------------------------------------------|---------------------------------------------------------------| | `channels` | `solutionChannels` | Decoded corridor assignments from the decoder | | `airports` | `airportsInRoutes` | Airport coordinates from encoder (same as problem visualizer) | | `file_output_dir` | `{"id": mapOutput.id, "ref": "datapool"}` | Datapool reference for saving the visualization | | `filename` | `"solution-visualization"` | Name for the generated solution map image | ::: tip Channel Difference Notice the solution visualizer uses `solutionChannels` from the decoder, which contains multiple corridors (Corridor 0, Corridor 1, etc.) with optimized route assignments. Each corridor is displayed in a different color, showing the conflict-free solution. ::: ### Save Your Work Click the **Save** button in the top left of the modeler. ## Part 5: Testing Your Workflow ### Step 0: Define the input schema Before executing the workflow, define the input schema for better validation and usability. ::: tip Learn More For a comprehensive explanation of how workflow services automatically generate their API from input schemas, see [Automatic API Generation](/services/workflow/api-generation). ::: 1. Select the **Start Event** (○) 2. In the properties panel, navigate to the **"API Description"** section 3. Paste the following JSON schema in the `Request Schema` field: ```json { "type": "object", "properties": { "flightRoutes": { "type": "array", "items": { "type": "object", "properties": { "origin": { "type": "string" }, "destination": { "type": "string" } }, "required": ["origin", "destination"] } }, "mapOutput": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "ref": { "type": "string", "enum": [ "datapool" ] } }, "required": ["id", "ref"], "additionalProperties": false } }, "required": ["flightRoutes", "mapOutput"] } ``` Based on this schema, the platform will validate your input when executing the workflow via the Service Jobs page. ### Step 1: Deploy the Workflow 1. In your workflow service, navigate to the "Workflow" tab 2. Click "Deploy" in the top left 3. Wait for deployment to complete (green toast will appear in the top right) ::: tip Save → Deploy After any change in the modeler, click **Save**. However, your changes will only be available in an execution, if you deployed the workflow. To deploy it, simply click on **Deploy** before running again. Deploy also saves the latest changes. ::: ### Step 2: Prepare Your Datapool 1. Navigate to **[Datapools](https://dashboard.hub.kipu-quantum.com/datapools)** (or use **Left nav: Data → Datapools** if the link fails) 2. Create a new datapool (or use an existing one). Name your datapool freely (e.g., `air-traffic-results-01`). 3. **Copy the datapool UUID** from the datapool details page ### Step 3: Execute the Workflow 1. On your workflow service page, click the green "Run Service" button in the top left :::tip As an alternative, you can navigate to the [Service Jobs](https://dashboard.hub.kipu-quantum.com/service-jobs) page and click on "Create Service Job". In the service dropdown, select your workflow service. ::: 2. **Input Mode:** Use **Manual JSON** and paste the sample below. Replace the datapool UUID. In the "Input Mode" section, use this test request (⚠️ Do not forget to replace the datapool ID): ```json { "flightRoutes": [ { "origin": "HEL", "destination": "FCO" }, { "origin": "BER", "destination": "MAD" }, { "origin": "CDG", "destination": "OTP" }, { "origin": "FCO", "destination": "CDG" }, { "origin": "OTP", "destination": "OSL" } ], "mapOutput": { "ref": "datapool", "id": "YOUR-DATAPOOL-UUID-HERE" } } ``` 3. Replace `YOUR-DATAPOOL-UUID-HERE` with your actual datapool UUID 4. Click "Create Job" 5. You'll automatically be redirected to the job details page ### Step 4: Monitor Execution **Where to find Service Jobs:** Navigate to **Left nav: Operations → Service Jobs**. 1. Navigate to the **"Service Jobs"** tab 2. Find your job in the list (most recent will be at the top) 3. Click on the job to see detailed execution progress 4. Watch as each service task completes: * ✅ Encode Flight Routes * 🔄 Solve Quantum Problem (this may take 1-2 minutes) * 🔄 Visualize Original Problem * ✅ Decode Quantum Solution * ✅ Visualize Optimized Solution ::: tip Parallel Execution in Action Notice that "Solve Quantum Problem" and "Visualize Original Problem" run simultaneously. This is parallel processing working as designed! ::: ::: warning Troubleshooting If your workflow fails, check these common issues: * Re-check **Part 4** output mappings (Process variable → Result variable order) * Confirm **Parallel** vs **Exclusive** gateway where indicated * Ensure **Personal** context (not "Kipu Quantum") * **Save → Deploy** again, then re-run * If the **encoder fails**, start with output mapping fixes ::: ### Step 5: View Results 1. When the workflow completes, navigate to your **datapool** 2. You should see two new files: * `problem-visualization.png` - Shows all original routes * `solution-visualization.png` - Shows routes organized into optimal corridors 3. If files don't appear immediately, **refresh** the datapool tab. 4. Preview and compare the images: * **Problem visualization**: All routes shown together (potential conflicts) * **Solution visualization**: Routes colored by corridor assignment (conflict-free) ## Part 6: Understanding the Results ### Reading the Visualizations #### Problem Visualization * All flight routes shown in the same color * You can see where routes intersect (potential collision points) * This represents the input to the optimization problem #### Solution Visualization * Routes colored by corridor assignment * Different colors = different corridors (vertically separated in real airspace) * Routes in the same corridor don't intersect * This represents the optimized solution from quantum computing ### What the Quantum Computer Did The Illay Base Quantum Optimizer service: 1. Explored many possible corridor assignments 2. Used quantum superposition to evaluate multiple solutions simultaneously 3. Found the assignment that minimizes conflicts 4. Returned the optimal solution ## Part 7: Returning Data from Workflows By default, your air traffic workflow saves visualization images to the datapool but doesn't return data via the API to external callers. To return corridor assignments and other results programmatically, configure output variables on the **End Event**: 1. Select the End Event (●) in your workflow 2. Navigate to the "Outputs" section in the properties panel 3. Add output variables using [FEEL expressions](./data-manipulation.md) The output variables become fields in your workflow's API response. For example, if you configure: * Variable name: `corridors` with expression: `solutionChannels` * Variable name: `quantum_result` with expression: `quantumSolution` * Variable name: `status` with expression: `"optimization_complete"` Your API will return: ```json { "corridors": [ { "channel": "Corridor 0", "routes": [ { "origin": "HEL", "destination": "FCO" } ] }, { "channel": "Corridor 1", "routes": [ { "origin": "BER", "destination": "MAD" } ] } ], "quantum_result": [ 1, 0, 1, 0, 1 ], "status": "optimization_complete" } ``` This enables external applications to consume the optimization results programmatically, in addition to the visual maps stored in your datapool. ## Part 8: Next Steps ### Extend the Workflow Now that you have a working air traffic management workflow, try these extensions: 1. **Multi-Objective Optimization**: Add fuel efficiency and delay minimization to the optimization goals 2. **Dynamic Route Updates**: Create a loop that re-optimizes when new flights are added 3. **Weather Integration**: Add a weather service that influences corridor assignments 4. **Real-Time Monitoring**: Connect to a live flight data API for real-world testing 5. **Comparative Analysis**: Run the same problem on multiple quantum backends and compare results ### Share Your Workflow 1. Add comprehensive documentation to your service description 2. Publish the workflow service to the Platform marketplace 3. Create example API calls in the service documentation 4. Share your results with the Platform community ### Learn More * **[FEEL Expressions Reference](https://docs.camunda.io/docs/components/modeler/feel/what-is-feel/)**: Advanced data transformations * **[BPMN 2.0 Specification](https://www.omg.org/spec/BPMN/2.0/)**: Complete BPMN reference * **[Platform API Documentation](https://docs.hub.kipu-quantum.com/)**: Integrate workflows into your applications ## Summary Congratulations! You've built a production-ready quantum workflow that: * ✅ Solves a real-world optimization problem * ✅ Uses quantum computing (Illay) for enhanced performance * ✅ Implements parallel processing for efficiency * ✅ Generates visual results for easy interpretation * ✅ Handles data flow between multiple services ### Key Takeaways 1. **Service Orchestration**: Workflows coordinate multiple services without custom code 2. **Parallel Processing**: Use parallel gateways to execute independent tasks simultaneously 3. **Data Flow**: Map inputs/outputs carefully using FEEL expressions 4. **Quantum Integration**: Illay makes quantum computing accessible through simple APIs 5. **Visualization**: Transform complex results into understandable visual formats ### What You Learned * Creating complex workflow control flows with parallel branches * Configuring service task inputs/outputs with nested data structures * Integrating quantum optimization services * Testing and debugging workflows * Best practices for production deployment *** ## Appendix: Quick Reference ### BPMN Elements Cheat Sheet | Element | Symbol | Purpose | When to Use | |---------------------------|------------------|----------------------------|--------------------------------------| | **Start Event** | ○ | Workflow entry point | Every workflow needs exactly one | | **End Event** | ● | Workflow completion | Mark successful completion | | **Platform Service Task** | ▢ | Execute a service | Call any subscribed Platform service | | **Parallel Gateway** | ◇+ | Split/merge parallel flows | Run tasks simultaneously | | **Exclusive Gateway** | ◇× | Conditional branching | Choose one path based on condition | | **Sequence Flow** | → | Execution order | Connect all elements | | **Error Boundary Event** | ⚡ on task border | Handle errors | Catch service failures | | **Timer Boundary Event** | ⏰ on task border | Handle timeouts | Prevent hanging tasks | ### Data Mapping Quick Reference #### Input Configuration ``` Local variable name: data Variable assignment value: { "field1": workflowVariable, "field2": "literal value" } ``` #### Output Configuration ``` Result variable name: outputVariable Process variable name: response.field ``` #### FEEL Expression Examples ```javascript // Variables (no quotes) variableName object.property // Literals (with quotes for strings) "string value" 123 true // Arrays and objects [1, 2, 3] { "key" : value } // Operations value1 + value2 count(array) string(number) // Conditions value > 10 count(routes) < 5 ``` ### Resources **Documentation**: * [FEEL Expressions](https://docs.camunda.io/docs/components/modeler/feel/what-is-feel/) * [BPMN 2.0 Specification](https://www.omg.org/spec/BPMN/2.0/) * [Platform Docs](https://docs.hub.kipu-quantum.com/) **Services**: * [Platform Marketplace](https://hub.kipu-quantum.com/marketplace) * [Service Subscriptions](https://dashboard.hub.kipu-quantum.com/applications) * [Datapools](https://dashboard.hub.kipu-quantum.com/datapools) --- --- url: /services/workflow/api-generation.md description: >- Define a JSON Schema on a workflow Start Event to auto-generate OpenAPI docs, validate inputs, and type the workflow service API. --- # Automatic API Generation Workflow services automatically generate their API based on the input schema you define. This makes it easy to create well-documented, validated services without manually writing API specifications. ## How It Works When you define an input schema on the **Start Event** in your workflow, the platform uses this schema to: 1. **Generate API Documentation**: Creates OpenAPI/Swagger documentation automatically 2. **Validate Requests**: Ensures incoming requests match your expected structure 3. **Provide Type Information**: Gives consumers clear information about required and optional fields 4. **Enable Better Testing**: The Service Jobs page uses the schema for input validation ## Defining the Input Schema To define your workflow's API, you need to specify a JSON Schema on the Start Event: ### Step-by-Step 1. **Select the Start Event** in your workflow (the circle ○ at the beginning) 2. **Open the Properties Panel** on the right side 3. **Navigate to "API Description"** section 4. **Paste your JSON Schema** in the `Request Schema` field ### Example Schema Here's a simple example for a workflow that processes flight routes: ```json { "type": "object", "properties": { "flightRoutes": { "type": "array", "items": { "type": "object", "properties": { "origin": { "type": "string" }, "destination": { "type": "string" } }, "required": ["origin", "destination"] } }, "mapOutput": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "ref": { "type": "string", "enum": ["datapool"] } }, "required": ["id", "ref"], "additionalProperties": false } }, "required": ["flightRoutes", "mapOutput"] } ``` This schema defines: * **flightRoutes**: An array of route objects, each with origin and destination (both required) * **mapOutput**: An object with datapool reference information (id and ref both required) * Both top-level properties are marked as required ## Benefits of Schema-Based API Generation ### 1. **Automatic Validation** When you execute your workflow through the Service Jobs page, the platform validates your input against the schema: * Missing required fields are caught before execution * Type mismatches are detected early * Invalid enum values are rejected ### 2. **Clear Documentation** The schema serves as documentation for service consumers: * Clear indication of required vs. optional fields * Type information for all properties * Validation rules (formats, enums, patterns) * Nested object structures ### 3. **Better Developer Experience** Developers using your service get: * IDE autocompletion (when schema is available) * Clear error messages when validation fails * Examples of valid request formats ### 4. **Version Control** The schema is part of your workflow definition: * Changes are tracked along with your workflow * Easy to see API changes over time * Rollback capabilities if needed ## JSON Schema Features You can use standard JSON Schema features to define your API: ### Types ```json { "type": "string" // Text values "type": "number" // Numeric values "type": "integer" // Whole numbers only "type": "boolean" // true/false "type": "array" // Lists of items "type": "object" // Nested structures } ``` ### Validation Rules ```json { "type": "string", "minLength": 3, "maxLength": 50, "pattern": "^[A-Z]{3}$", // Airport codes: JFK, LAX, etc. "format": "uuid" // Predefined formats } ``` ### Required Fields ```json { "type": "object", "properties": { "name": { "type": "string" }, "age": { "type": "integer" } }, "required": ["name"] // age is optional } ``` ### Enums ```json { "type": "string", "enum": ["datapool", "local", "external"] } ``` ### Arrays ```json { "type": "array", "items": { "type": "object", "properties": { "origin": { "type": "string" }, "destination": { "type": "string" } } }, "minItems": 1, "maxItems": 100 } ``` ## Best Practices ### 1. **Use Descriptive Property Names** Choose clear, self-explanatory names: ✅ `flightRoutes`, `destinationAirport`, `departureTime` ❌ `data`, `input1`, `x` ### 2. **Add Descriptions** While not shown in the basic example, you can add descriptions: ```json { "type": "object", "properties": { "flightRoutes": { "type": "array", "description": "List of flight routes to optimize", "items": { ... } } } } ``` ### 3. **Be Specific with Validation** Use appropriate validation rules: ```json { "type": "string", "format": "uuid", // For IDs "pattern": "^[A-Z]{3}$" // For airport codes } ``` ### 4. **Mark Fields as Required Appropriately** Only mark fields as required if they're truly necessary: ```json { "required": ["flightRoutes"], // Must have routes // mapOutput might be optional if results can be returned directly } ``` ### 5. **Use additionalProperties Wisely** Control whether extra fields are allowed: ```json { "type": "object", "properties": { ... }, "additionalProperties": false // Reject unknown fields } ``` ## Testing Your Schema After defining your schema: 1. **Save** your workflow 2. **Deploy** to make the schema active 3. **Run Service** from the Service Jobs page 4. The platform will validate your input against the schema 5. Fix any validation errors that appear ## Accessing the Generated API Once deployed, your workflow service has: * **API Endpoint**: Available through the platform * **OpenAPI Specification**: Auto-generated from your schema * **Validation**: Automatic request validation * **Documentation**: Available to service consumers ## Related Topics * [Introduction & Tutorial](/services/workflow/air-traffic-tutorial) - Learn about workflow services with a complete example * [Data Manipulation](/services/workflow/data-manipulation) - Working with data in workflows * [Describe your API](/services/managed/openapi) - API documentation for managed services * [Service Configuration](/services/managed/service-configuration) - General service configuration ## Next Steps Now that you understand API generation: 1. Define a schema for your workflow's start event 2. Test the validation with sample inputs 3. Share your service with clear API documentation 4. Iterate based on consumer feedback --- --- url: /services/workflow/data-manipulation.md description: >- Transform, filter, and aggregate workflow data using FEEL expressions for variables, strings, numbers, comparisons, and arrays. --- # Data Manipulation in Workflows Data manipulation is a crucial aspect of workflow management, allowing users to transform, filter, and aggregate data as it moves through various stages of a workflow. This document outlines the key concepts and techniques for effective data manipulation within workflows. ### FEEL Expressions for Data Manipulation Kipu Quantum Hub workflows use [FEEL (Friendly Enough Expression Language)](https://docs.camunda.io/docs/components/modeler/feel/what-is-feel/) for data manipulation. Here are the most common expressions you'll use: #### Accessing Variables ```javascript variableName // Access a variable object.property // Access nested property array[1] // Access array element (0-indexed) get or else(variableName, "DefaultValue") // Provide default value if variable is not defined ``` #### String Operations ```javascript "Hello " + name // Concatenate strings string(value) // Convert to string upper case(text) // Convert to uppercase lower case(text) // Convert to lowercase ``` #### Number Operations ```javascript count(array) // Count array elements sum(numbers) // Sum array of numbers 5 + 3 // Addition 10 * 2 // Multiplication ``` #### Comparison Operations ```javascript value > 10 // Greater than value < 5 // Less than value = 10 // Equals (single =) value != 10 // Not equals ``` #### Logical Operations ```javascript condition1 and condition2 // Both conditions true condition1 or condition2 // At least one condition true not condition // Negate condition ``` #### Working with Arrays ```javascript [1, 2, 3] // Create array array[1] // Access element count(array) // Array length for i in array return i * 2 // Transform array ``` #### Creating Objects ```javascript { "key": value, // Using variable value "name": "literal" // Using literal string } ``` #### Working with Secrets You can request to inject a secret value into a `$secrets` input variable mapping by using the `secret_value` function. ```javascript { "ibm_token": secret_value("field_name_in_json_input") } ``` #### Common Patterns **Referencing workflow variables** (no quotes): ```javascript flightRoutes // Variable from Start Event or previous task ``` **Literal values** (with quotes for strings): ```javascript "binary" // String literal 1000 // Number literal true // Boolean literal ``` **Nested property access**: ```javascript quantumMapping.routes // Access 'routes' inside 'quantumMapping' map_output.id // Access 'id' inside 'map_output' ``` **Building complex objects**: ```javascript { "id": datapool.id, "ref": "datapool", "timestamp": now() } ``` --- --- url: /services/workflow/common-workflow-compositions.md description: >- Patterns for composing BPMN workflows including fan-out/fan-in, pipelines, and conditional flows using exclusive gateways. --- # Common Workflow Compositions Understanding common workflow compositions helps you design better workflows and solve problems more effectively. ## 1. Fan-Out/Fan-In (Parallel Processing) ``` ○ → [Prepare Data] → ◇+ → [Process A] → ◇+ → [Combine Results] → ● └ → [Process B] → ┘ ``` **Use when**: Processing the same data with multiple services simultaneously **Benefits**: * Faster execution (parallel instead of sequential) * Independent processing paths * Results synchronized automatically **Example**: Execute a quantum circuit on multiple backends to compare results. ## 2. Pipeline (Sequential Processing) ``` ○ → [Step 1] → [Step 2] → [Step 3] → [Step 4] → ● ``` **Use when**: Each step depends on the previous step's output **Benefits**: * Clear data flow * Simple to understand and debug * Each step transforms data for the next **Example**: Generate circuit → Encode → Execute → Decode → Visualize ## 3. Conditional Flow (Exclusive Gateway) ``` ○ → [Check Condition] → ◇× → [Path A] → ● └→ [Path B] → ● ``` **Use when**: Different actions needed based on data or conditions **Benefits**: * Dynamic workflow behavior * Handle different scenarios * Optimize based on input characteristics **Example**: Use fast optimization for small problems, accurate optimization for large problems. **Setting Conditions**: 1. Add an **Exclusive Gateway** (◇×) from the palette 2. Create multiple outgoing paths 3. Select each sequence flow (arrow) 4. In properties, set **Condition expression**: ```javascript = count(flightRoutes) < 5 // Fast path = count(flightRoutes) >= 5 // Accurate path ``` ## 4. Error Handling with Boundary Events ``` ○ → [Main Task] → [Success Action] → ● │ └→ [Error Handler] → [Cleanup] → ● ``` **Use when**: You need to handle failures gracefully **Benefits**: * Graceful degradation * User notification on errors * Cleanup and recovery actions See [Handling Errors in Workflow Services](./error-handling.md) for the full contract — which BPMN errors are raised, how the boundary auto-wires to `SERVICE_FAILED`, and how to read the failure payload via the per-host `_serviceError` variable. ### 5. Looping (Multi-Instance) ``` ○ → [For Each Item in Array] → [Process Item] → [Collect Results] → ● ``` **Use when**: You need to process array data iteratively **Benefits**: * Process collections automatically * Parallel or sequential execution * Aggregate results **Implementation**: 1. Select a service task 2. In properties, find **Multi Instance** 3. Set **Loop Type**: Parallel or Sequential 4. Set **Input Collection**: Array variable name 5. Set **Element Variable**: Name for current item **Example**: Process each flight route individually for detailed analysis. ## 6. Timer Events (Scheduled Execution) ``` ○ (clock icon) → [Periodic Task] → [Process Data] → ● ``` **Use when**: You need periodic or scheduled execution **Benefits**: * Automated scheduling * Time-based triggers * Periodic monitoring **Types**: * **Timer Start Event**: Trigger workflow on schedule * **Timer Intermediate Event**: Wait for duration * **Timer Boundary Event**: Timeout handling **Example**: Run optimization every hour with latest flight data. ## 7. Message Events (External Triggers) ``` ○ (envelope icon) → [Wait for Message] → [Process] → ● ``` **Use when**: Workflows triggered by external systems **Benefits**: * Event-driven architecture * Integration with external systems * Asynchronous communication **Use Cases**: * API webhooks * External data updates * User actions --- --- url: /services/workflow/secrets.md description: >- Pass SecretValue parameters securely to orchestrated services by defining a $secrets input variable in your workflow configuration. --- # Using Secrets in Workflow Services This guide explains how to securely pass secrets to orchestrated services within your workflow, ensuring sensitive information like API tokens and credentials are handled safely. ## Overview When orchestrating services that require secret inputs, workflow services provide a special mechanism to securely pass these sensitive values without exposing them in logs or workflow variables. ## How Secrets Work in Workflows In workflow services, you can orchestrate services that require `SecretValue` parameters by: 1. **Defining a `$secrets` input variable** in your orchestration configuration 2. **Mapping workflow input fields to orchestrated service secret parameters** 3. **The platform automatically handles** the secure injection of secret values This approach maintains security while allowing workflows to coordinate services that need sensitive credentials. ## Basic Secret Mapping Syntax To pass secrets to an orchestrated service, define a `$secrets` input variable with a mapping structure: ```python { "ibm_token": secret_value("field_name_in_json_input") } ``` **Structure Explanation**: * **Key** (`ibm_token`): The secret parameter name expected by the orchestrated service * **Value** (`secret_value("field_name_in_json_input")`): Maps to a field in the workflow's JSON input where the secret value will be provided ## Example: Orchestrating a Service with Secrets ### Scenario You have a workflow that orchestrates a quantum service requiring an IBM Quantum token for authentication. ### Orchestrated Service The target service expects a secret parameter: ```python from qhub.commons.secret import SecretValue def run(circuit_data: dict, ibm_token: SecretValue) -> dict: """ Executes a quantum circuit on IBM Quantum hardware. """ token = ibm_token.unwrap() # Use token for IBM Quantum API authentication ... ``` ### Workflow Service Let's imagine we just use a workflow service to orchestrate the service from above, simply like so: ``` ○ → [Orchestrated Service] → ● ``` ### Workflow Input JSON As the `Orchestrated Service` requires a `circuit_data` dictionary and an `ibm_token` secret, we could define to expose those parameters in the workflow input JSON like this: ```json { "circuit_data": { "qubits": 5, "gates": ["H", "CNOT"] }, "$secrets": { "ibm_api_token": "your-secret-ibm-token-here" } } ``` ### Input Mapping Go to the input mapping of the `Orchestrated Service` in the workflow editor. Click **+** for each input mapping: | Local variable name | Variable assignment value | Description | |---------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------| | `circuit_data` | `circuit_data` | Direct mapping of the workflow input to the orchestrated service. | | `$secrets` | `{ "ibm_token": secret_value("ibm_api_token") }` | Map secret `ibm_api_token` from workflow input to `ibm_token` of the orchestrated service. | ## How It Works Under the Hood 1. **Workflow receives input** with secret values in designated JSON field (`$secrets`) 2. **Platform extracts secret values** from the specified input fields (`secret_value("field_name_in_json_input")`) 3. **During execution**, the actual secret values are stored securely in a secure storage system designed for sensitive data 4. **Secrets are injected** as environment variables into the orchestrated service runtime 5. **Orchestrated service receives** `SecretValue` objects that wrap the sensitive data 6. **After execution**, secret values are purged from the secure storage system 7. **Security is maintained** throughout the orchestration chain --- --- url: /services/workflow/error-handling.md description: >- Catch platform service-task failures with a boundary error event and consume the failure envelope via the auto-injected per-host `_serviceError` process variable. --- # Handling Errors in Workflow Services When a platform service task in your workflow does not complete successfully, the workflow raises a BPMN error. You decide what happens next: catch the error and continue on an alternative path, or let it escape so it surfaces on the workflow's `/result` response. ## Outcomes of a Platform Service Task Every platform service task ends in one of two terminal outcomes. Only the unsuccessful one raises a BPMN error. | Outcome | BPMN error? | BPMN error code | When it happens | |-------------|-------------|------------------|----------------------------------------------------------------------------------------------| | `SUCCEEDED` | no | — | The service returned a result. | | `FAILED` | yes | `SERVICE_FAILED` | The service reported a failure (validation error, runtime error, backend rejection). | ## The Error Envelope When a service-task error is raised, an envelope describing the failure travels with it. You read this envelope inside the recovery branch (as a per-host `_serviceError` variable — see [Accessing the Error Envelope Downstream](#accessing-the-error-envelope-downstream)), and API consumers see the same shape in the workflow's `/result` response when an error escapes uncaught. ```json { "errorCode": "HUB_SERVICE_FAILED", "serviceId": "", "applicationId": "", "failedElement": "", "message": "", "response": { /* the upstream service's verbatim response body */ } } ``` Field rules: * `errorCode`, `failedElement`, and `message` are always present. * `errorCode` is one of `HUB_SERVICE_FAILED`, `WORKFLOW_ERROR`, or a modeler-defined BPMN error code from a modeler-authored error end event. * `serviceId` and `applicationId` are present only for `HUB_SERVICE_FAILED`. * `response` is present only for `HUB_SERVICE_FAILED`; it carries the upstream service's verbatim response body (RFC 7807 problem+json plus any HAL fields the managed-service contract attaches). * `message` is derived via the fallback chain `response.detail` → `response.title` → `response.message` → literal `"Service execution failed"`, so it is always a non-empty string regardless of upstream shape. ::: tip Two error-code layers, different audiences `SERVICE_FAILED` is the **BPMN-level** code BPMN uses internally to match catch handlers. `HUB_SERVICE_FAILED` is the **envelope-level** code your API consumers read off the `/result` body. They name the same thing in two different layers. ::: ## Reserved workflow output variable names A handful of identifiers are reserved on workflow end-event output mappings because the runtime or the response shape already uses them at the same scope. An end-event `target` matching any of these names is rejected at deploy time with a `422` (`ReservedOutputVariableException`); the workflow-modeler also catches the collision inline as you type via a properties-panel validator and at lint time via the `reserved-output-variable-name` bpmnlint rule. | Name | Why reserved | |----------------------|-------------------------------------------------------------------------------------------------------------------------------| | `executionId` | `PlatformWorkflowVariable` constant the runtime injects into workflow scope; writing it would overwrite platform state. | | `executionState` | `PlatformWorkflowVariable` constant the runtime injects into workflow scope; writing it would overwrite platform state. | | `serviceError` | `PlatformWorkflowVariable` constant carrying the failure envelope on a caught path; writing it would overwrite platform state.| | `triggeringTenantId` | `PlatformWorkflowVariable` constant the runtime injects into workflow scope; writing it would overwrite platform state. | | `errors` | Failure-branch discriminator key on the workflow `/result` response root; an output named `errors` would silently miscategorise a successful response as a failure. | | `_links` | HAL sibling at the response root; an output named `_links` would overwrite the HAL link block at serialisation time. | | `_embedded` | HAL sibling at the response root; an output named `_embedded` would overwrite the HAL embedded block at serialisation time. | Pick any other identifier — for example `failure`, `errorInfo`, `errorDetails` — when you want to surface the envelope on a recovery end event. ## Catching an Error Attach a **Boundary Error Event** to a platform service task — that is the entire catch configuration. The modeler automatically: * creates or reuses a global error named **Service Failed** with code `SERVICE_FAILED` and wires the boundary's **Error Reference** to it, and * arranges for the platform to capture the failure envelope into a per-host process variable named `_serviceError` when the workflow is deployed. `` is the BPMN id of the protected platform service task, for example `Activity_QuantumSim_serviceError`. The only modelling work that remains is the recovery path that follows the boundary. ::: tip Customise the payload variable name Select the boundary event to open its properties panel. Under the **Error** group, the **Payload variable** field shows the default name (`_serviceError`) and lets you rename it to a friendlier identifier — for example `coinTossError`. Stick with the default unless you have a reason to override it; the default is unique per host so multi-boundary workflows are safe by construction. Names must use letters, digits, or `_` only, and must start with a letter or `_`. No spaces, hyphens, dots, or `$` — the variable is consumed in both JUEL (`${...}` at runtime) and FEEL (recovery-path expressions), and only this character set is safe in both. ::: ::: info Why per-host naming Each boundary lifts the failure envelope into its own variable scoped by the host task's id, so multiple boundaries in the same workflow — parallel branches, sequential recovery — never overwrite each other's payload. Pick a custom name only on boundaries where you're confident no other boundary writes to the same name. ::: ### Steps in the modeler 1. Select the platform service task you want to protect. 2. Use **Append → Boundary Event**, then switch its type to **Error Boundary Event**. 3. Draw a sequence flow from the boundary event to the first element of your recovery path. 4. Connect the recovery path to either the workflow's existing end event (to recover into `SUCCEEDED` with normal output) or to a dedicated recovery end event (to surface the failure detail to the API consumer). ![Error Boundary Event on the "Execute Quantum Circuit" platform service task, routing to an "Inform Support Staff" recovery service; the properties panel shows the auto-wired Global error reference "Service Failed" and Code "SERVICE\_FAILED"](./error-boundary-auto-wired.png) ## Accessing the Error Envelope Downstream The variable is a JSON object available to every element on the recovery path: end events, gateways, script tasks. Its name is `_serviceError` by default, where `` is the BPMN id of the protected task; the boundary's **Payload variable** field shows the exact name (and lets you rename it). Throughout this section, the example host id `Activity_QuantumSim` stands in for whatever id your protected task carries. The patterns below cover the two common consumption modes. ::: info Different from task result handling Unlike a platform service task's result (which lives inside the task's execution and needs explicit Output mapping to escape), the error envelope is already lifted to process scope by the runtime. You don't author an Output mapping on the boundary itself — the variable is ready to read in any downstream FEEL expression as-is. For that reason, the modeler hides the **Output mapping** group on platform error boundaries: it would emit expressions the runtime can't evaluate. Rename via the **Payload variable** field on the boundary, or do any shaping on a downstream task. ::: ### Pass it through unchanged on a recovery end event The simplest pattern — expose the envelope to the API consumer as a top-level field on the response. 1. Select the end event of the recovery branch. 2. Open **Output mapping** in the properties panel and add an entry. 3. **Process variable name**: `failure` (or any non-reserved field name you want on the response — see [Reserved workflow output variable names](#reserved-workflow-output-variable-names)). 4. **Variable assignment value**: `= Activity_QuantumSim_serviceError` (use your task's id from the boundary panel). The `/result` response then carries the envelope under the field name you chose. To expose only a single field instead of the whole envelope, use [FEEL](./data-manipulation.md) dot notation in the **Variable assignment value** — for example `= Activity_QuantumSim_serviceError.message` returns just the human-readable description. ### Branch on the envelope Place an exclusive gateway on the recovery path and set sequence flow conditions on its outgoing flows: ```javascript = Activity_QuantumSim_serviceError.response.errorType = "validation" = Activity_QuantumSim_serviceError.response.errorType != "validation" ``` ## When You Don't Catch If no boundary event matches the raised error, the error escapes and the failed branch parks at the failing task. The workflow does **not** terminate — the process instance stays alive with an open **incident** describing the failure. Three things follow from that: * The workflow's reported state becomes `FAILED` (any open incident counts as a failure, even if other parallel branches are still running). * The `/result` endpoint returns the failure envelope under `errors[]` (see [Workflow Result Response Shape](#workflow-result-response-shape)). * Parallel branches that have not failed continue to run independently. A failure on one branch does not stop the others. ::: info Why the instance stays alive Keeping the process instance alive after an uncaught error is intentional. It leaves a future repair feature able to act on the still-active process (resume, retry, modify). You cannot resume manually today, but the runtime state is preserved. ::: ## Workflow Result Response Shape `GET /workflow-service-executions/{id}/result` returns the workflow's user output variables at the response root on success and an `errors[]` array at the response root on failure. Both shapes carry HAL siblings `_links.self`, `_links.status`, and `_embedded.status` alongside the user content. Discriminate the two branches by presence of the top-level `errors` key: if it is present, the workflow failed; otherwise it succeeded. There is no top-level `status` field. ### On success ```json { "": "...", "": "...", "_links": { "self": { "href": "https://.../service-executions/{id}/result" }, "status": { "href": "https://.../service-executions/{id}" } }, "_embedded": { "status": { /* full ServiceExecution */ } } } ``` Top-level user keys equal the output variables you mapped on the workflow's platform end events. ### On failure ```json { "errors": [ { "errorCode": "HUB_SERVICE_FAILED", "serviceId": "...", "applicationId": "...", "failedElement": "T2_quantum_sim", "message": "", "response": { /* upstream service's verbatim response body */ } } ], "_links": { "self": { "href": "https://.../service-executions/{id}/result" }, "status": { "href": "https://.../service-executions/{id}" } }, "_embedded": { "status": { /* full ServiceExecution */ } } } ``` `errors` is always an array — see [Parallel Branches and Multiple Failures](#parallel-branches-and-multiple-failures). Pre-terminal calls (before the workflow has reached `SUCCEEDED`, `FAILED`, or `CANCELLED`) return `404` with a "result not yet available" message. ## Parallel Branches and Multiple Failures When two branches run in parallel and both fail without being caught, each failure produces its own envelope. Both appear in the `errors[]` array: ``` ┌──▶ [ T1 quantum sim A ] ──┐ │ ✗ FAILED │ ( )──▶ ◇ split ─┤ ├── ◇ join ──▶( ) │ │ └──▶ [ T2 quantum sim B ] ──┘ ✗ FAILED ``` ```json { "errors": [ { "errorCode": "HUB_SERVICE_FAILED", "failedElement": "T1_quantum_sim_a", "message": "...", "response": { } }, { "errorCode": "HUB_SERVICE_FAILED", "failedElement": "T2_quantum_sim_b", "message": "...", "response": { } } ], "_links": { "self": { "href": "..." }, "status": { "href": "..." } }, "_embedded": { "status": { } } } ``` Each entry is sourced from its own task's failure, so payloads never overwrite each other. If only one of the parallel branches fails, `errors[]` has exactly one entry. The same isolation applies to the caught path: when each branch has its own boundary error event, the per-host `_serviceError` variables are distinct, so concurrent failures across branches don't clobber one another — `T1_quantum_sim_a_serviceError` and `T2_quantum_sim_b_serviceError` are independent. ## Throwing Your Own Errors Stop the workflow with a custom error at any point by using an **Error End Event** at the process root. 1. Drop an **End Event** on the canvas and change its type to **Error End Event**. 2. In the properties panel, define the BPMN error it throws: * **Error Code**: a constant like `VALIDATION_FAILED` that names the failure category. * **Error Message**: a FEEL expression that resolves to a human-readable string at throw time, for example `= validationDetail`. When the workflow reaches this end event, the error follows the same rules as a service-task error: catch it with a boundary handler, or let it escape into the failure response. If uncaught, the response shape is: ```json { "errors": [ { "errorCode": "VALIDATION_FAILED", "failedElement": "raiseValidationFailed", "message": "" } ], "_links": { "self": { "href": "..." }, "status": { "href": "..." } }, "_embedded": { "status": { } } } ``` Your `errorCode` value is carried verbatim into the envelope, so API consumers can distinguish your custom categories from `HUB_SERVICE_FAILED`. ## Choosing What to Catch * **Attach a boundary when you have a meaningful recovery path.** Fallback solvers, alternative backends, default values, partial-result branches — anything where you can do something useful with the failure detail. * **Leave the boundary off when surfacing the failure to the API consumer is the right answer.** An uncaught failure becomes an `errors[]` entry on `/result`, which is often exactly what external callers want. ## Related Topics * [Common Workflow Compositions](./common-workflow-compositions.md) — patterns for fan-out/fan-in, pipelines, and conditional flows. * [Data Manipulation](./data-manipulation.md) — FEEL expressions for reading the envelope. * [Automatic API Generation](./api-generation.md) — how the `/result` schema is generated for the workflow's OpenAPI spec. --- --- url: /services/on-premise/introduction.md description: >- Integrate, commercialize, and monetize self-hosted quantum services on Kipu Quantum Hub while keeping them on your own infrastructure. --- # Introduction On-premise services allow you to integrate, commercialize, and monetize your self-hosted quantum services via Kipu Quantum Hub. Your service can be hosted on the infrastructure of your choice and the platform manages the access and billing for you. ## Create an On-premise Service To create an on-premise service, go to the [create service page](https://dashboard.hub.kipu-quantum.com/services/new) and provide the following information: | Property | Description | |------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Name | Choose a meaningful name for your service. If you publish your service later on, this name will be displayed to other users. | | Service Type | Select "On-premise Service". | | Service Endpoint | Enter the public endpoint (URL) of your service. | | Security Configuration | Define how the platform authenticates requests to your service. At the moment, Basic Authentication using username and password is supported. | | API Specification | Click on "Import from OpenAPI File" if you already have prepared an OpenAPI specification for your service. You can leave this empty for now and supply an OpenAPI specification later. | | Description | Provide any additional meaningful information you want to provide to other users. | Finally, click on "Create Service" to create your service. --- --- url: /services/on-premise/publish-marketplace.md description: >- Add an OpenAPI description, create a pricing plan, and publish an on-premise service to the Kipu Quantum Hub Marketplace. --- # Publish on Marketplace Once you have created your service, you can offer it to other users by publishing it on the Kipu Quantum Hub Marketplace. To publish your service, follow the steps below. ## Create an API Description using OpenAPI Specification v3.0 Each on-premise service needs to provide a description of the service interface and input data to let other users discover and understand the capabilities of your service. Further, this is the technical baseline for the platform to integrate your on-premise service. The platform uses the [OpenAPI Specification v3 (OAS3)](https://swagger.io/specification) to describe the API of an on-premise service. You can change the API description of your service at any time by clicking on `Edit Service` on the service details page. ## Create a Pricing Plan A pricing plan for an on-premise service consists of the products that you, as a service provider, want to charge your customers for. For example, if you want to charge for API calls, CPU time, and memory time, create a product for each of these. To charge your customers, [report the usage to our Metering API](report-usage.md). You can create a pricing plan for your service by following these steps: 1. On the details page of your service, click on `Create Pricing Plan`. 2. On the create pricing plan page, add your products to the pricing plan. For each product provide the following information: * **Name**: The name of the product, e.g, `CPU Time`. * **Unit Price**: The price per unit of the product, e.g., `0.0001` EUR. * **Unit**: The unit describes how the product is sold and appears as a label on customer's invoices. For example, if your product is `CPU Time` and is billed *per second*, the unit would be `second`. 3. Click on `Create Plan`. 4. On the services details page you will see your pricing plan with its products. ### Publish your Service to the Marketplace Finally, to publish your service to the Marketplace, click on `Publish to Marketplace` on the service details page. Now, other users can discover and subscribe to your service. --- --- url: /services/on-premise/report-usage.md description: >- Report usage events from on-premise services to the Kipu Quantum Hub Metering API so customers can be billed per product item. --- # Report Service Usage To charge your customers for using your service, you need to report the usage to our Metering API. The platform aggregates all reported usage events and charges your customers at the end of each month. ## Authentication The Metering API uses access tokens to authenticate requests. You can view and manage your personal access tokens in your [settings](https://dashboard.hub.kipu-quantum.com/settings/access-tokens). For authentication, provide your access token in the `X-Auth-Token` header field for each request. ## `POST /qc-catalog/external-services/metering` This endpoint is used to report the usage of your on-premise service. The request body must contain a `correlationId`, which is forwarded by the platform API Gateway upon service execution. The platform then logs a usage event for the corresponding product item (`productId`) and the submitted count. **Request Body:** ```json { "correlationId": "string", "productId": "string", "count": 0 } ``` * The `correlationId` is needed to correlate your reported usage to the corresponding user of your service. You can obtain the correlation id from the `x-correlation-id` header of the request that was forwarded by our API Gateway to your service. * The `productId` is the id of the product you want to report. You can find the id of your product in the pricing plan table on the service details page. * The `count` is the quantity of units you want to report. **Example:** ```shell curl -X 'POST' 'https://api.hub.kipu-quantum.com/qc-catalog/external-services/metering' \ -H 'Accept: */*' \ -H 'Content-Type: application/json' \ -H 'X-Auth-Token: ' \ -d '{ "correlationId": "bXlleHRlcm5hbHNlcnZpY2VuYW1lOnRoZWFwcGxpY2F0aW9ubmFtZXRoYXRpc3N1YnNjcmliZWQ=", "productId": "prod_YX8skS2X", "count": 10 }' ``` ## Test your metering logic To verify that your service is correctly reporting usage to the Metering API, you can use the Metering Test Mode. You can use the test mode by following these steps: 1. On the service details page, click on `Publish Internal`. This will make your service accessible only to you. 2. Subscribe to the service using one of your [Applications](https://dashboard.hub.kipu-quantum.com/applications). 3. Execute the service. 4. Your service logic need to obtain the correlation id from the `x-correlation-id` header of the request that was forwarded by our API Gateway to your service. 5. Meter the usage of your service by calling the Metering API with the correlation id you obtained in the previous step. 6. On the service details page, click on `Metering Events`. This will show you the metering events that were reported to our Metering API. --- --- url: /agentic/mcp-server.md description: >- Connect AI assistants like Claude, Cursor, and GitHub Copilot to Kipu Quantum Hub via the hosted MCP server to manage quantum resources in natural language. --- # Agent-Ready with MCP Server The Kipu Quantum Hub is now agent-ready, meaning you can access and manage your quantum computing resources directly through your favorite AI assistants like Claude, GitHub Copilot, and other AI-powered development tools. This opens up exciting new possibilities for working with quantum computing in a more natural, conversational way. Imagine describing what you want to achieve in plain language and having your AI assistant handle the complexities of job submission, circuit optimization, and result analysis. Whether you're prototyping quantum algorithms, managing computational resources, or analyzing experimental results, your AI agent can now assist you every step of the way. ## The Kipu Quantum Hub MCP Server The **Kipu Quantum Hub MCP Server** is a hosted Model Context Protocol (MCP) server that bridges AI assistants with the Kipu Quantum Hub platform. It runs remotely at `https://api.hub.kipu-quantum.com/mcp` — there is nothing to install. MCP is an open standard that enables AI assistants to securely connect to external tools and data sources, extending their capabilities beyond conversation into real-world actions. By connecting your AI assistant to the Kipu Quantum Hub MCP server, you give it the ability to: * **Access your quantum resources** — query available quantum hardware, check system status, and view your computational quotas * **Manage quantum jobs** — submit, monitor, and retrieve results from quantum computations * **Work with quantum circuits** — help you design, optimize, and execute quantum algorithms * **Streamline your workflow** — handle routine tasks so you can focus on the science and innovation The server exposes its capabilities as tools in two namespaces: * `hub_*` — core Hub platform API (services, data pools, applications, use cases, …) * `quantum_*` — Quantum Workloads API (quantum jobs, sessions, backends, …) Plus a top-level `run_subscribed_service` tool to invoke services you are subscribed to. ## Prerequisites * An MCP-capable AI client (Claude Desktop, Claude Code, Cursor, VS Code with an MCP extension, …) * A Kipu Quantum Hub account — sign up at ## Authentication The server authenticates via OAuth. The first time your AI client connects, it opens a browser window where you log in to the Kipu Quantum Hub and authorize access. No tokens or credentials are stored in your config files. If your client does not open the login page automatically, trigger the connection (for example, list the server's tools) and follow the authorization link it provides. ## Configure Your AI Agent The server is a remote HTTP MCP server at `https://api.hub.kipu-quantum.com/mcp`. Add it to your client using the snippets below, then restart the client to load it. On first use you will be prompted to authorize access in your browser. ### Claude Desktop Open **Settings → Connectors → Add custom connector** and enter the URL: ``` https://api.hub.kipu-quantum.com/mcp ``` ### Claude Code Register the server with the CLI: ```shell claude mcp add --transport http qhub-mcp https://api.hub.kipu-quantum.com/mcp ``` Or add it manually to `.mcp.json` (per-project) or `~/.claude.json` (global): ```json { "mcpServers": { "qhub-mcp": { "type": "http", "url": "https://api.hub.kipu-quantum.com/mcp" } } } ``` ### Cursor Edit `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per-project): ```json { "mcpServers": { "qhub-mcp": { "url": "https://api.hub.kipu-quantum.com/mcp" } } } ``` ### VS Code Add to your user or workspace `settings.json` (requires an MCP-capable extension such as GitHub Copilot Chat in agent mode): ```json { "mcp": { "servers": { "qhub-mcp": { "type": "http", "url": "https://api.hub.kipu-quantum.com/mcp" } } } } ``` ### Other MCP clients Any MCP client that supports remote HTTP (streamable) servers can connect to: * `url`: `https://api.hub.kipu-quantum.com/mcp` * `transport` / `type`: `http` The client handles the OAuth login on first connect. ## What You Can Do With the Kipu Quantum Hub MCP server connected to your AI assistant, you can: ### Natural Language Quantum Computing Describe what you want to accomplish in natural language, and your AI assistant will help translate that into quantum operations on the Kipu Quantum Hub. ### Streamlined Workflows * Quickly submit quantum jobs without context-switching between tools * Monitor multiple jobs and get intelligent summaries of their status * Retrieve and analyze results with AI-assisted interpretation ### Intelligent Resource Management * Ask about available quantum hardware and get recommendations * Check your computational quotas and resource usage * Plan and schedule quantum experiments efficiently ### Development Assistance * Get help designing quantum circuits with real-time feedback * Debug quantum algorithms with AI-powered analysis * Optimize circuits for specific quantum hardware architectures ## Example Use Cases > "What quantum backends are currently available on the Kipu Quantum Hub? Which one would be best for a 20-qubit circuit?" > "I want to learn about use cases for quantum computing in finance. What use cases are described on the Kipu Quantum Hub?" > "Help me find suitable services on the Kipu Quantum Hub for my optimization problem." > "Retrieve the results from my last three quantum jobs and compare their results." > "How do I get started using Kipu's Illay Base Quantum Optimizer?" ## Additional Resources * **Endpoint**: `https://api.hub.kipu-quantum.com/mcp` * **Dashboard**: *** Ready to supercharge your quantum computing workflow with AI? Connect the Kipu Quantum Hub MCP server and start exploring what's possible when you combine the power of quantum computing with intelligent assistance. --- --- url: /agentic/usecase-to-service-agentic.md description: >- Build, test, and deploy a quantum service end-to-end using qhubctl and the Kipu Quantum Hub MCP server with an AI coding assistant. --- # Tutorial: Quantum Application Development with Kipu Quantum Hub MCP Server In this tutorial, you will learn how to develop quantum services using the Kipu Quantum Hub MCP server. You will learn how to use the MCP server to: 1. Find suitable use cases that match your business domain and objectives 2. Discover relevant quantum services on the Kipu Quantum Hub 3. Implement quantum applications using the identified services and use cases 4. Test your implementation locally 5. Deploy your service to Kipu Quantum Hub In this tutorial, we use the **airline industry** as an example business domain. ## Prerequisites * **qhubctl** installed (see [CLI installation guide](../cli-reference)) * **Access to Kipu Quantum Hub** (sign up at ) * **Personal Access Token** for Kipu Quantum Hub * **AI coding assistant** that supports MCP servers (e.g., Claude Code, GitHub Copilot, etc.). We will use Claude Code in this tutorial, but feel free to use any AI coding assistant that supports MCP servers ## 1. Setup We will use our CLI `qhubctl` to set up a new `Python Starter` project for the Kipu Quantum Hub. ### Create a New Project Create a new project using the following command: ```bash qhubctl init ``` You will be prompted to provide some information about your project. For this tutorial, select: * **Service name**: Choose a meaningful name (e.g., `airline-optimization-service`) * **Starter template**: `Python Starter` * **vCPU configuration**: `1 vCPU` * **Memory configuration**: `1GB` This will create a new directory containing all required files to run your quantum code on Kipu Quantum Hub. ### Configure the MCP Server Once the project is created, add the Kipu Quantum Hub MCP server to your AI coding assistant. **For Claude Code users:** The project is already set up with a `.mcp.json` file in your project directory that points at the hosted MCP server: ```json { "mcpServers": { "qhub-mcp": { "type": "http", "url": "https://api.hub.kipu-quantum.com/mcp" } } } ``` On first use, Claude Code opens a browser window where you log in to the Kipu Quantum Hub and authorize access. **For other AI coding assistants:** Please refer to the documentation of your AI coding assistant on how to add MCP servers. See the [MCP Server documentation](mcp-server.md) for more details. ## 2. Identifying Relevant Use Cases Once everything is set up, you can start by asking your AI coding assistant to help you identify relevant use cases for quantum computing. The MCP server provides access to the Kipu Quantum Hub's use case repository, which contains industry-specific quantum computing applications. ### Example Prompt Ask your AI coding assistant: > What are some relevant use cases for quantum computing in the airline industry? Please provide a list of use cases along with a brief description of each. Your AI assistant will query the Kipu Quantum Hub MCP server and return relevant use cases. For example: > Based on my search of the Kipu Quantum Hub platform, here are relevant quantum computing use cases for the airline industry: > > 1. Multi-Aircraft Routing > Finding collision-free routes for multiple aircraft with defined start and destination locations. Aircraft move through a predefined grid structure to avoid conflicts. > Application: Air traffic management, flight path optimization > Method: Quantum annealing approaches > > 2. Single Vehicle Routing > Pathfinding along grid points from start to destination using quantum annealing algorithms. > Application: Individual flight route optimization, ground vehicle routing at airports > Method: Quantum annealing Review the use cases and select the one that best aligns with your business objectives. For this tutorial, we'll focus on the **Multi-Aircraft Routing** use case. ## 3. Discovering Quantum Services Now that we've selected the \*\*Multi-Aircraft Routing \*\* use case, we need to find quantum services on the Kipu Quantum Hub that can solve this type of problem. The MCP server enables your AI assistant to search through available services and understand their capabilities. ### Example Prompt Ask your AI coding assistant with a use case-specific prompt: > For the Multi-Aircraft Routing use case, find quantum services offered by Kipu Quantum on the Kipu Quantum Hub that are suitable for solving this routing optimization problem. Please provide a list of services along with their descriptions and capabilities. Your AI assistant will search the Kipu Quantum Hub marketplace and return relevant services that can handle routing optimization problems. Here are some example services it might find: > **Based on the Multi-Aircraft Routing use case, here are some relevant quantum services available on the Kipu Quantum Hub:** > > 1. Illay Base Quantum Optimizer *(FREE)*:\ > Free quantum optimizer using **DCQO** (Digitized Counterdiabatic Quantum Optimization) on IBM Aer simulator. > > 2. Miray Advanced Quantum Optimizer *(PREMIUM)*:\ > Cutting-edge bias field quantum optimization running on actual IBM quantum hardware. Uses the **bf-DCQO** (bias-field Digitized Counterdiabatic Quantum Optimization) algorithm for production-scale problems. > > *All services accept **QUBO/Ising** problem formulations, which is the standard format for encoding the Multi-Aircraft Routing constraints (collision avoidance, time-based routing, path optimization) as described in the use case documentation.* The AI assistant can also provide details about: * Service input/output formats * Supported quantum backends and hardware * Pricing information * Documentation and code examples * How the service maps to the Multi-Aircraft Routing problem ## 4. Implementing Quantum Applications Now that you have identified a use case and discovered relevant services, it's time to implement your quantum application. Your AI coding assistant can help you write the code that integrates with the selected quantum service. ### Example Prompt For the Air Traffic Management use case, you might prompt: > Help me implement a managed service for the Kipu Quantum Hub that uses the Illay Base Quantum Optimizer to solve an air traffic management problem. > Implement a small example 5 flight routes with some intersections. Ensure that it is feasible to be solved with the Iskay Base Quantum Optimizer. > The following constraints must be considered: > > * Minimize the number of vertical corridors used > * Intersecting routes cannot share the same corridor. > * Minimize total distance, fuel consumption, or time based on corridor assignments > Please use the service we found earlier and implement the encoding, execution, and decoding steps. For implementation use the project we are currently in. ### What Your AI Assistant Will Do Your AI assistant will: 1. **Analyze the service API**: Understand the input/output format required by the service 2. **Write the encoding logic**: Convert your problem (flight routes, intersections) into a format the quantum service accepts 3. **Implement service execution**: Use the qhub-service library to execute a service 4. **Write the decoding logic**: Convert the quantum solution back into flight corridor assignments 5. **Add error handling**: Ensure robust execution with proper exception handling ### Working Collaboratively with Your AI Assistant As you implement your application: * Ask the AI assistant to explain the quantum service's input format * Request help with data transformations * Ask for code improvements and optimizations * Get assistance with debugging and error handling The AI assistant has access to the service documentation through the MCP server, so it can provide accurate, up-to-date implementation guidance. ## 5. Testing Locally Before deploying your service to Kipu Quantum Hub, test it locally to ensure it works correctly. ### Install Dependencies First, navigate to your project directory and install the required dependencies: ```bash cd airline-optimization-service ``` Create a virtual environment and install dependencies: ```bash # Using uv (recommended) uv venv uv sync source .venv/bin/activate # Or using pip python -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` ### Test Using Python Directly Run your service directly with Python: ```bash python -m src ``` This will execute your service with the default input data defined in `input/data.json` and `input/params.json`. ### Test Using the CLI For a more realistic test that simulates the platform environment, use `qhubctl`: ```bash qhubctl serve ``` This starts a local server on `http://localhost:8081`. Once the server is operational, you can access . This interface provides you the ability to run your current code and see the results. Further information can be found in the [CLI reference](../cli-reference#qhubctl-serve). ### Get Help from Your AI Assistant If you encounter errors during testing, ask your AI assistant for help: > I'm getting an error when testing my service locally: \[paste error message]. Can you help me debug this? Your AI assistant can analyze the error, check service documentation via the MCP server, and suggest fixes. ## 6. Deploying to Kipu Quantum Hub Once you have tested your service locally and verified it works correctly, deploy it to Kipu Quantum Hub. ### Deploy Using the CLI From your project directory, run: ```bash qhubctl up ``` This command will: 1. Package your project (respecting `.qhubignore` exclusions) 2. Upload it to Kipu Quantum Hub 3. Trigger the containerization process 4. Create a managed service After successful deployment, you will see a message with the service URL. ### Verify Your Deployment 1. Navigate to the [Services page](https://dashboard.hub.kipu-quantum.com/services) on Kipu Quantum Hub 2. Find your newly created service in the list 3. Click on it to view the service details 4. Wait for the containerization to complete (status will change to "RUNNING") ### Execute Your Deployed Service Execute your service using the CLI: ```bash qhubctl run ``` This will: * Use the input data from `input/data.json` and `input/params.json` * Execute the service on Kipu Quantum Hub * Display the execution status and result ## Next Steps Congratulations! You have successfully: * Identified quantum computing use cases for your domain using the MCP server * Discovered relevant quantum services on Kipu Quantum Hub * Implemented a quantum application with AI assistance * Tested your implementation locally * Deployed your service to Kipu Quantum Hub ## Additional Resources * [qhubctl CLI Reference](../cli-reference.md) * [MCP Server Documentation](mcp-server.md) * [Managed Services Documentation](../services/managed/introduction.md) * [Quickstart Guide](../quickstart.md) * [Service SDK Reference](../sdk-service.md) --- --- url: /manage-quantum-jobs.md description: >- Monitor, cancel, and retrieve results of quantum jobs submitted via the SDK or by managed services through the Quantum Jobs dashboard. --- # Manage Quantum Jobs Gain a comprehensive overview of all quantum jobs or tasks you have submitted using the SDK by visiting the [Quantum Jobs](https://dashboard.hub.kipu-quantum.com/quantum-jobs) page. If you need to view the jobs submitted by an organization you are a member of, simply switch your account context by clicking on your name in the **upper right corner** of the page. ## Job Actions By clicking the action button on the right side of each job, you can perform the following actions: * **Retrieve Inputs & Results**:\ Download your quantum job inputs and results (after the job has completed) directly through the UI. * **Cancel Jobs**:\ Cancel jobs that are still queued at the backend. This feature helps you save costs, especially if expensive jobs are accidentally submitted to costly backends. ## Managing Service Jobs As a service host, you can view the input data and results of jobs initiated by your service’s executions. Additionally, you have the ability to cancel any jobs that are queued from a service execution. Follow these steps to view jobs associated with a specific service execution: 1. Click on "Applications" tab in the main menu. If you want to access the jobs of your organization, ensure that you selected it, in the top left corner. 2. Choose your subscribed service from the list of subscribed services. 3. In the "Subscriptions" section, click on "Activity Logs". 4. Locate the relevant service execution and click on "Show Jobs". ::: tip NOTE Due to confidentiality reasons, you **cannot** access jobs from service executions initiated by external users or organizations, even if you are hosting the service. ::: --- --- url: /usecases/introduction.md description: >- Document industrial quantum use cases with details, authors, demos, sketches, and relations to algorithms and services on Kipu Quantum Hub. --- # Use Cases When you have worked on an industrial use case which exploits quantum algorithms for solving and improving one (or even multiple) subproblem(s) you can elaborate on it in this section. When creating a new use case and after entering its name, you should see 4 tabs at the top, whose contents are described below: ## Details Most of the important information of your use case must be noted here (otherwise you will not be able to publish your use case). The summary should contain a *very* short description of the use case, which will be displayed on the preview tile within the quantum service store. It is limited to 200 characters (less than a tweet!), so keep it simple! Anything that goes beyond that can be (and should be!) exhaustively described in the eponymous field "Description". Similar to the description of an algorithm, you can put all information regarding the use case (e.g. how to get from the initial problem statement to the corresponding mathematical subproblem, which can be mapped onto quantum hardware) in here.\ For illustration purposes you can also add some pictures within "Sketches" and reference them within the description field. Also, you should add some application areas and industries, which might be relevant for the use case at hand ## Authors The Authors section allows you to properly attribute all contributors who worked on the use case. You can add both individual persons and organizations as authors to give appropriate credit and provide contact points for those interested in learning more about your work. When adding authors, make sure to include all key contributors who played a significant role in developing, implementing, or documenting the use case. This helps establish credibility and enables the community to connect with the experts behind the use case for potential collaborations or inquiries. ## Demos For each use case, you have the option to create an interactive demo that allows users to experience and experiment with your quantum solution firsthand. Interactive demos provide a hands-on way for the community to understand how your use case works in practice, making complex quantum algorithms more accessible and tangible. Demos can showcase the inputs, parameters, and outputs of your quantum application, enabling users to try different configurations and see real-time results. This interactive approach significantly enhances understanding and engagement with your use case. For detailed information on how to create and configure demos, including technical requirements and best practices, please refer to the [Demos documentation](./demos/introduction.md). ## Sketches The Sketches section enables you to upload visual materials such as diagrams, flowcharts, architecture diagrams, and other images that help illustrate your use case. Visual representations are often crucial for explaining complex quantum workflows, system architectures, or the mapping from classical problems to quantum formulations. You can upload sketches directly to this section and then reference them within your use case description using Markdown image syntax. This allows you to create a rich, illustrated documentation that makes your use case more understandable and engaging for readers. For more information on how to reference images in Markdown and use advanced formatting features, please refer to the [Markdown documentation](../references/markdown-latex-editor.md). ## Relations The Relations section is a powerful feature that allows you to establish connections between your use case and the underlying algorithms and services that implement it. By linking relevant algorithms and services to your use case, you create a comprehensive knowledge graph that helps users understand the complete technical solution. These relationships provide valuable context by showing: * Which quantum algorithms are applied to solve the problem * Which services implement these algorithms and can be used to execute the solution * How different components work together to address the use case Establishing these relations makes it easier for the community to explore the technical implementation, discover reusable components, and understand the end-to-end solution architecture. This interconnected approach enhances discoverability and helps users navigate from high-level business problems to concrete technical implementations. --- --- url: /usecases/demos/introduction.md description: >- Overview of Demos, interactive web interfaces for quantum use cases deployed from GitHub or GitLab repositories on Kipu Quantum Hub. --- # Introduction Demos make it easy for you to create and host interactive web interfaces for your quantum and machine learning use cases. To deploy a Demo, simply connect a GitHub or GitLab repository. The platform automatically builds and deploys your Demo every time you push to the default branch of your repository. A simple way to create a Demo is to use our [Gradio starter template](https://github.com/planqk/planqk-demo-starter-gradio). But, you can also deploy any other web app of your choice using Docker. --- --- url: /usecases/demos/deploy-demo.md description: >- Connect a GitHub repository to deploy a Demo, with required host, port, and CORS configuration for Kipu Quantum Hub integration. --- # Deploy a Demo On this page you will learn how to deploy a Demo for your Use Case. You will learn how deployments work, the requirements for host and port configuration, and how to deploy a Demo step-by-step. ## How deployments work To deploy a Demo, simply connect a GitHub repository. The platform always **deploys the default branch** of your repository and **automatically triggers a re-deployment** every time you **push to the default branch**. Each deployed Demo has a resource limit of 1 CPU and 512 MiB of memory and automatically scales to zero when not in use. ## Host and port configuration Your Demo application must listen for requests on `0.0.0.0` on the port `8080`. ### CORS configuration Your webserver configuration must allow cross-origin requests (CORS) from Kipu Quantum Hub. This is necessary to allow users to view your Demo application direclty via the platform . ::: warning Besides the configuration of the `Access-Control-Allow-Origin` and the `Access-Control-Allow-Methods` corresponding to your demo application, you **must** allow the following headers in your CORS policy: * `Authorization` * `Content-Type` * `X-OrganizationId` ::: #### Gradio CORS configuration An example CORS configuration for a Gradio app would look like this: ```python import gradio as gr def your_function(input): return "Hello, " + input app = gr.Interface(fn=your_function, inputs="text", outputs="text") # Launch with custom server settings and CORS headers app.launch( server_name="0.0.0.0", server_port=8080, allowed_origins=["*"], allowed_headers=["Authorization", "Content-Type", "X-OrganizationId"] ) ``` #### NGINX CORS configuration An example configuration using NGINX as a webserver could look like this: ```nginx server { listen 0.0.0.0:8080 default_server; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; # CORS Headers add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-OrganizationId' always; # Handle preflight if ($request_method = OPTIONS) { return 204; } } } ``` ## A step-by-step guide to deploy a Demo A simple way to create a Demo is to use the [Gradio](https://www.gradio.app) python library. Gradio lets you build interactive web interfaces in a matter of minutes. Check out our [Gradio starter template](https://github.com/planqk/planqk-demo-starter-gradio). Alternatively, you can deploy any other web app of your choice using Docker. The following steps show you how to deploy a Demo for your Use Case. Prerequisites: * A [Use Case](https://dashboard.hub.kipu-quantum.com/use-cases) you want to deploy a Demo for. Alternatively, create a new Use Case. * A fork of our [Gradio starter template](https://github.com/planqk/planqk-demo-starter-gradio). **To deploy a Demo** for your Use Case click on the Demo tab of your Use Case and click on the **Create Demo** button. You will be asked to connect your GitHub account (if you haven't done so already) and to select a repository. Select the fork of the Gradio starter template you created earlier by clicking on **Connect**. That's it, you deployed your first Demo! But there is **one more thing**. In order to make your Demo work we need to set some environment variables. [Learn how to set environment variables](environment-variables.md). --- --- url: /usecases/demos/environment-variables.md description: >- Configure environment variables in Demo settings to pass Application credentials and other secrets to your deployed demo code. --- # Set Environment Variables If your demo requires environment variables (for instance, [Application credentials](../../services/applications)), you can set them in the**Settings** of your Demo. You can access them in your code like regular environment variables, for example with `os.getenv()` in Python. As an example, below is an excerpt of our [Gradio starter template](https://github.com/planqk/planqk-demo-starter-gradio): ```python import os from qhub.service.client import HubServiceClient access_key_id = os.getenv('ACCESS_KEY_ID', None) secret_access_key = os.getenv('SECRET_ACCESS_KEY', None) service_endpoint = "https://gateway.hub.kipu-quantum.com/anaqor/quantum-random-number-generator/1.0.0" def run(n_numbers: int): client = HubServiceClient(service_endpoint, access_key_id, secret_access_key) execution = client.run(request={"data": data, "params": params}) execution.wait_for_final_state() result = execution.result() random_number_list = result["result"]["random_number_list"] return ", ".join([str(x) for x in random_number_list]) ``` The code calls the [Quantum Random Number Generator Service](https://hub.kipu-quantum.com/marketplace/services/88b46e18-3d5f-4674-ba04-0d3416c0decd) using the [service-sdk](https://pypi.org/project/qhub-service). The service is available as a free service on the Kipu Quantum Hub Marketplace. To access the service, a [subscription to the service](../../services/using-a-service) is needed. We can add the Access Key ID and Secret Access Key of the subscribed Application as environment variables to the Demo. --- --- url: /usecases/demos/starter-templates.md description: >- Community-contributed Gradio, Nuxt, and SPA starter templates for bootstrapping Demo applications on Kipu Quantum Hub. --- # Starter Templates Together with our community, we have created a set of starter templates for the most popular frameworks to help you get started with your Demo. To use one of the starter templates, simply fork the repository and connect it to your demo. Each starter template provides a README with instructions on how to use it. We are happy to add more starter templates contributed by the community. If you have created a starter template for a framework that is not listed below, please let us know via our [Discord Server](https://discord.gg/qhwDBPpuFE). * [Gradio Starter](https://github.com/planqk/planqk-demo-starter-gradio): A template using **Gradio**, a python library that lets you build interactive web interfaces in a matter of minutes. * [Nuxt Starter](https://github.com/planqk/planqk-demo-starter-nuxt): A template using **Nuxt**, a **Vue** framework that supports creating API endpoints to securely access your services. * [SPA Starter](https://github.com/viralitygmbh/planqk-webapp-demo-template): A template for SPAs such as **Angular**, **React** with a proxy server to securely access your services. --- --- url: /manage-organizations.md description: >- Create organizations, invite members with Viewer, Maintainer, or Owner roles, and switch account context for team collaboration. --- # Manage Organizations Organizations allow you to collaborate with your team. Besides their individual accounts, users of the platform can also be associated with some organization, e.g. as an employee of a company. In that context, users might want to be able to, e.g., publish algorithms or services as well as execute jobs either as an individual or as part of such an organization. In the drop-down menu of your personal account in the top right corner you can select the section "Organizations". Besides the ones your account is already associated to, you also have the option to create a new organization, which requires a name and a billing address. Under the menu item "Members" you can easily add members to the organization and assign them one of multiple roles (similar to the roles associated to an algorithm or an implementation): * `Viewer`: Can see the content of the organization, but cannot edit or create content. * `Maintainer`: Can create new content, edit existing content, but cannot delete content. * `Owner`: Can create, edit and delete hole content of the organization. Owners can add new members, assign them different roles, and can delete members. Of course, an owner can see the entire organization profile and edit it. The added member will receive an email invitation that must be confirmed. After that, the member can see the organization in his account context drop-down menu. ## Switch Context between Personal Account and Organization Assuming you are a member of an organization, you should be aware of the **Account Context** in the top left corner, right above the different sections whenever you are doing something on the platform. **Note**: When you are not a member of an organization you will not see the context-drop-down menu associated to it. ::: tip IMPORTANT * As of now, after creating a new service, algorithm, etc. you are **NOT** able to change its context. So, before you do something new, be sure to have selected the correct context for the content you are about to create. * Refreshing your browser resets the context to your personal account. ::: --- --- url: /manage-access-tokens.md description: >- Create and manage Personal Access Tokens for platform API access and Provider Access Tokens for bring-your-own quantum backend credentials. --- # Manage Access Tokens Access tokens are used in token-based authentications to allow users to access the platform API or to let the platform at runtime access the API of a quantum backend provider. Kipu Quantum Hub supports two types of access tokens: (1) **Personal Access Tokens** for accessing the platform API, e.g., by the [CLI](cli-reference) to automate the interaction with the platform or by the [Quantum SDK](sdk-quantum) to develop and execute quantum circuits using our Qiskit extension, and (2) **Provider Access Tokens** to allow the platform accessing the API of quantum backend providers at runtime. This is especially useful when you want to execute your quantum solutions using your own accounts for certain quantum backends (bring your own token). ## Personal Access Tokens You can use personal access tokens to access the platform API, e.g., by the [CLI](cli-reference) or by the [Quantum SDK](sdk-quantum). Further, you can use them to authenticate any custom application that wants to interact with the platform API. To create a personal access token to your account, go to the user-menu in the top right corner and click on "Settings". Under "Personal Access Tokens" you can create new personal access tokens and manage existing ones. ::: tip NOTE Personal access tokens can only be created for user accounts. You can use your personal access token to interact with organizations you are a member of. ::: ## Provider Access Tokens By bringing your own access tokens, you can use your own accounts for certain quantum backends. This allows the platform to access quantum backend providers at runtime. To add a token for your account, go to the user-menu in the top right corner and click on "Settings". Under "Provider Access Tokens" you can add different tokens to your account, depending on the provider. Alternatively, when you are an owner or maintainer of an organization, you can provide access tokens in the section "Provider Access Tokens" of your organization settings. If provided, every member of the organization can run circuits/jobs with these access tokens. ::: tip IBM backends with your own token When you bring your own IBM token, do **not** use the `HubQiskitRuntimeService` from the [Quantum SDK](sdk-quantum). That class is only for accessing IBM backends through Kipu Quantum Hub's managed access. Instead, use IBM's plain `QiskitRuntimeService` from the `qiskit-ibm-runtime` package directly. See [Bring Your Own IBM Token](sdk-quantum#bring-your-own-ibm-token) for details. ::: --- --- url: /manage-git-integrations.md description: >- Connect GitHub and GitLab accounts to your user or organization to import repositories and enable automatic builds on the Kipu Quantum Hub. --- # Manage Git Integrations Git integrations connect your Kipu Quantum Hub account or organization to GitHub and GitLab so you can import repositories and enable automatic builds, e.g., for [Demos](usecases/demos/deploy-demo). You can manage Git integrations in \[User Icon] → `Settings` → `Git Integrations`. ::: tip NOTE Adding a Git integration to an organization requires the `Maintainer` role in that organization on the platform. See [Manage Organizations](manage-organizations) for details on roles. ::: Access tokens used by Git integrations are stored securely and encrypted. Token values are never visible in plain text after they are saved. ## GitHub Connecting a GitHub account installs the Kipu Quantum Hub GitHub App on a GitHub user account or organization. The GitHub App grants the platform access to the repositories you select during installation. **To connect a GitHub account**, open the Git Integrations page and click **Connect GitHub Account**. You will be redirected to GitHub to authorize the GitHub App and choose which repositories the platform can access. ### Required GitHub permissions * To install the GitHub App on a **personal GitHub account**, you must be the owner of that account. * To install the GitHub App on a **GitHub organization**, you must have the `Owner` role in that GitHub organization. If you don't have owner rights, you can request access — a GitHub organization admin can then approve the installation on your behalf. ### Add by installation ID If the GitHub App has already been installed on a GitHub account or organization (for example by an admin), you can attach the existing installation to your account or organization on the platform via the **Add by installation ID** link on the Git Integrations page. ## GitLab The GitLab integration uses a [GitLab personal access token](https://docs.gitlab.com/user/profile/personal_access_tokens/) to access your GitLab repositories. **To add a GitLab token**, open the Git Integrations page and click **Add GitLab Token**, then paste a personal access token from GitLab. ### Required GitLab token scopes * **`api` (required)**: Needed so the platform can read repositories and import them. * **Maintainer access on the repository (required)**: Needed so the platform can **auto-configure automatic rebuilds** of a Demo when you push to the repository. --- --- url: /references.md description: >- Landing page for Kipu Quantum Hub SDKs and CLI. Pick the right tool (qhub-quantum, qhub-service, qhub-api, qhubctl) for your task with a decision matrix and install cheat-sheet. --- # References Overview Kipu Quantum Hub ships **three SDKs and one CLI**. Each one targets a different job. This page helps you pick the right one in under a minute. ## Which tool should I use? | I want to… | Use this | Package | |------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|---------------------------------------| | Run a Qiskit or Braket circuit on a quantum backend | [**Quantum SDK**](../sdk-quantum.md) | `qhub-quantum` *(Python)* | | Use IBM Qiskit Runtime sessions/primitives | [**Quantum SDK**](../sdk-quantum.md#using-hubqiskitruntimeservice) — `HubQiskitRuntimeService` | `qhub-quantum` *(Python)* | | Submit raw jobs, manage sessions, or inspect backends without Qiskit | [**API SDK**](../sdk-api-quantum.md) — `HubQuantumClient` | `qhub-api` *(Python, TypeScript)* | | Browse the service catalog, manage data pools, applications, or subscriptions | [**API SDK**](../sdk-api-platform.md) — `HubPlatformClient` | `qhub-api` *(Python, TypeScript)* | | Call a Managed Service you (or someone else) deployed (submit jobs, get results, stream logs) | [**Service SDK**](../sdk-service.md) | `qhub-service` *(Python, TypeScript)* | | Log in, bootstrap a project, run it locally, or deploy it to the Hub | [**CLI**](../cli-reference.md) | `@quantum-hub/qhubctl` *(Node.js)* | | Configure a service project (name, resources, runtime) | [**qhub.json**](../qhub-json-reference.md) | file in project root | | Style descriptions in the platform UI with Markdown or LaTeX | [**Markdown & LaTeX**](../references/markdown-latex-editor.md) | — | ## At a glance ### Quantum SDK — `qhub-quantum` Run **Qiskit 2.2** or **Amazon Braket** circuits on any [supported backend](https://hub.kipu-quantum.com/quantum-backends). Provides drop-in replacements for Qiskit/Braket providers: * `HubQiskitProvider` — gate-based backends (IonQ, IQM, Rigetti, simulators) * `HubQiskitRuntimeService` — IBM backends with sessions, Sampler/Estimator * `HubBraketProvider` — AWS-accessed devices (including QuEra Aquila for AHS) * Works with **PennyLane** via the `pennylane-qiskit` plugin → [Open reference](../sdk-quantum.md) ### Service SDK — `qhub-service` High-level client for **Managed Services** you (or others) have deployed on the Hub. Submit executions, poll status, fetch results and logs, download result files, cancel runs, and attach short-lived **Data Pool grants**. * Available for **Python and TypeScript** — shared `HubServiceClient` surface. * Python ships a richer `HubServiceExecution` wrapper with `wait_for_final_state`, `result()`, `result_files()`, and `logs()` helpers. * Authenticates to the service gateway via OAuth 2.0 client credentials (access key + secret); uses a personal access token only when requesting data pool grants. → [Open reference](../sdk-service.md) ### API SDK — `qhub-api` Typed, multi-language clients for the **Kipu Quantum Hub REST APIs**. One package ships three clients — one per API surface — generated from the same OpenAPI definitions: * `HubQuantumClient` — submit jobs, manage sessions, inspect backends and calibration. * `HubPlatformClient` — browse the catalog (services, algorithms, applications, data pools, subscriptions, grants, users, marketplace). * `HubServiceClient` — invoke a deployed service through the gateway with a short-lived bearer token. * Available for **Python 3.9+** and **Node 18+ / ESM**. * Authenticates with a personal access token (Platform/Quantum) or a short-lived bearer token (Service gateway). → [Open reference](../sdk-api.md) ### CLI — `qhubctl` Terminal tool for **project lifecycle** on the Hub. Log in, bootstrap a new service, run it locally, deploy it, upload files to a data pool, inspect build status. * Installed via `npm i -g @quantum-hub/qhubctl` * Used by the other SDKs for auth: `qhubctl login -t ` makes subsequent SDK calls token-free → [Open reference](../cli-reference.md) ## Install cheat-sheet ::: tabs key:pythonTS \== Python ```bash # Quantum SDK (Qiskit / Braket) pip install --upgrade qhub-quantum # Service SDK uv add qhub-service # or: pip install qhub-service # API SDK uv add qhub-api # or: pip install --upgrade qhub-api ``` \== TypeScript ```bash # Service SDK npm install @quantum-hub/qhub-service # API SDK npm install @quantum-hub/qhub-api ``` ::: ```bash # CLI npm i -g @quantum-hub/qhubctl qhubctl login -t ``` ## Authentication at a glance | Tool | Credential | How to supply it | |-------------|--------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Quantum SDK | Personal access token | `qhubctl login` **or** `access_token=...` argument | | API SDK | Personal access token (Platform/Quantum) or bearer token (Service) | Passed to the constructor (`api_key=...` / `apiKey` for Platform/Quantum; `token=...` for Service). Python helpers (`DefaultCredentialsProvider`) can resolve from env vars or the `qhubctl` config file — not wired into constructors automatically. | | Service SDK | Service-scoped access key + secret | Passed to the constructor (`access_key_id`, `secret_access_key`). In Python, the personal access token used for data pool grants is auto-resolved from `qhubctl` / env when available. | | CLI | Personal access token | `qhubctl login -t ` | Generate personal access tokens on the [Access Tokens page](https://dashboard.hub.kipu-quantum.com/settings/access-tokens). Service access keys are generated per-application in [Service Settings](https://dashboard.hub.kipu-quantum.com/services). ## What's next? * New here? Start with the [Quickstart](../quickstart.md). * Deploying your own service? Read [Implementations → Create a Service](../implementations/create-a-service.md). * Looking for a specific backend? See [Available Backends](https://hub.kipu-quantum.com/quantum-backends). --- --- url: /sdk-quantum.md description: >- Run Qiskit and Amazon Braket circuits on Kipu Quantum Hub backends via HubQiskitProvider, HubQiskitRuntimeService, and HubBraketProvider. --- # Quantum SDK Reference > \[!INFO] At a glance > > * **Purpose:** Run Qiskit or Amazon Braket circuits on Kipu Quantum Hub backends. > * **Package:** `qhub-quantum` *(Python 3.11+)* > * **Entry points:** `HubQiskitProvider` · `HubQiskitRuntimeService` *(IBM only)* · `HubBraketProvider` *(AWS/Braket, incl. QuEra Aquila)* > * **Use it when:** you write circuits with Qiskit, Braket, or PennyLane. > * **Don't use it when:** you need to call a Managed Service → use the [Service SDK](/sdk-service). > * **Auth:** personal access token via `qhubctl login` or constructor argument. > * **New here?** See the [References Overview](/references/). The Quantum SDK provides an easy way to develop quantum code that runs on [quantum hardware and simulators supported](https://hub.kipu-quantum.com/quantum-backends) by the [Kipu Quantum Hub](https://hub.kipu-quantum.com). The SDK supports both the [Qiskit 2.2 SDK](https://github.com/Qiskit/qiskit) and the [Amazon Braket SDK](https://github.com/amazon-braket/amazon-braket-sdk-python), allowing you to choose your preferred framework for quantum programming: * **Qiskit**: Access all gate-based quantum backends and simulators provided by Kipu Quantum Hub. * [**HubQiskitProvider**](#using-hubqiskitprovider): Standard Qiskit provider for direct backend access (except IBM backends) * [**HubQiskitRuntimeService**](#using-hubqiskitruntimeservice): Qiskit Runtime for IBM backend access and session-based operations * **Amazon Braket SDK**: Access all devices provided by Kipu Quantum Hub through AWS, such as the QuEra Aquila quantum device. * [**HubBraketProvider**](#using-amazon-braket): Braket provider for AWS quantum devices (required for QuEra Aquila) This integration enables you to seamlessly adapt and reuse your existing Qiskit or Braket code within the Kipu Quantum Hub environment, maximizing productivity while working with the frameworks you are already accustomed to. If you are using [PennyLane](https://pennylane.ai) to implement your quantum machine learning algorithms, you can use the [SDK along with the PennyLane-Qiskit plugin](#pennylane-integration) to run them on the quantum hardware provided by Kipu Quantum Hub. ## Installation You need to have Python 3.11 or higher installed. The package is released on PyPI and can be installed via `pip`: ```bash pip install --upgrade qhub-quantum ``` > \[!TIP] > Ensure that you have versions older than Qiskit SDK 2.2 uninstalled before installing the Quantum SDK. > The best practice is to create a new virtual environment and freshly install the SDK. After installation, follow the section for your preferred framework: [Qiskit](#using-qiskit) or [Amazon Braket](#using-amazon-braket). ## Authentication To use the SDK, you need to authenticate using an access token. You may use your personal access token found on the platform [welcome page](https://dashboard.hub.kipu-quantum.com/home), or you can generate dedicated [access tokens](https://dashboard.hub.kipu-quantum.com/settings/access-tokens). An access token can be set in two ways: 1. Automatically, by logging in through [qhubctl](quickstart#login-to-your-account). The command to login via CLI is `qhubctl login -t `. This method will automatically inject the access token when you instantiate the `HubQiskitProvider` or `HubBraketProvider` class. If you want to log in with your organization you need to additionally execute `qhubtl set-context` and select the organization. 2. Explicitly, during instantiation of the `HubQiskitProvider` or `HubBraketProvider` class. This method overrides any access token that has been automatically injected through qhubctl login. You can optionally pass the organization id as a parameter, if you want to execute your circuit using your organization's account. If the access token is not set, is invalid, or has expired, an `InvalidAccessTokenError` is thrown. You need to generate a new token and log-in again. ## Using Qiskit The SDK provides two Qiskit providers ([see overview](#accessing-quantum-backends)). ### Using HubQiskitProvider In your Python code you can access the Kipu Quantum Hub quantum backends through the `HubQiskitProvider` class. Import the class and instantiate it as shown below: ```python from qhub.quantum.sdk import HubQiskitProvider ``` If you are already logged in with [qhubctl](quickstart#login-to-your-account) you can create the provider object without any parameters: ```python provider = HubQiskitProvider() ``` Alternatively, you can also create the provider object by passing a personal access token as a parameter: ```python provider = HubQiskitProvider(access_token="YOUR_PERSONAL_ACCESS_TOKEN_HERE") ``` If you want to log in with your organization, you can additionally pass the organization id as a parameter. The organization id can be found in the organization settings on the platform: ```python provider = HubQiskitProvider(organization_id="YOUR_ORGANIZATION_ID_HERE", access_token="...") ``` > \[!NOTE] > IBM backends must be accessed using the [`HubQiskitRuntimeService`](#using-hubqiskitruntimeservice) instead of the `HubQiskitProvider`. > This applies when accessing IBM **through Kipu Quantum Hub**. > If you want to run on IBM with **your own IBM token** (bring your own token), do **not** use the Hub SDK — use IBM's plain `QiskitRuntimeService` instead, as described in [Bring Your Own IBM Token](#bring-your-own-ibm-token). > To programmatically determine which provider to use for a specific backend, use `backends(detailed=True)` as described in [Discovering Provider Support](#discovering-provider-support). #### Use the Provider Class After you have created the provider object, you can list all backends supported by Kipu Quantum Hub and select the one you want to use. The available backends and their ids can be also found [here](https://hub.kipu-quantum.com/quantum-backends): ```python # List all available quantum backends backends = provider.backends() # Select a certain backend backend = provider.get_backend("kipu.sim.qsim") ``` > \[!TIP] > To access other QPUs, either you or your organization must have payment information added to your account. > To upgrade your account, go to your [Account Settings](https://dashboard.hub.kipu-quantum.com/settings/account), click the > *Upgrade* button, and follow the prompts to enter your payment details. #### Execute a Quantum Circuit Now you can execute your Qiskit circuit on the selected backend, retrieve its `job` object, retrieve its results, or cancel it. The full example would look like this: ```python{17} from qhub.quantum.sdk import HubQiskitProvider from qiskit import QuantumCircuit, transpile provider = HubQiskitProvider() backend = provider.get_backend("kipu.sim.qsim") # Create a Qiskit circuit circuit = QuantumCircuit(3, 3) circuit.h(0) circuit.cx(0, 1) circuit.cx(1, 2) circuit.measure(range(3), range(3)) circuit = transpile(circuit, backend) job = backend.run(circuit, shots=100) # Monitor job status and get results print(f"Status: {job.status()}") print(f"Result: {job.result()}") ``` > \[!IMPORTANT] > Executing your quantum circuits or programs on Kipu Quantum Hub may lead to execution costs depending on selected backend and number of shots. > Please find an overview about the costs for each backend [on our pricing page](https://kipu-quantum.com/platform/pricing/). #### Compilation Modes on AWS Braket Backends This section applies to AWS Braket backends only; other providers ignore the flags. | Mode | Flags | Who compiles the circuit | | --- | --- | --- | | Default | none | Braket, server-side, from the device's abstract gate set. | | Native | `native=True` plus `optimization_level` or `pass_manager` | Qiskit, client-side, against the backend target. | | Verbatim | `verbatim=True` | Nobody — the circuit is submitted exactly as given. | In default mode the hardware provider routes and compiles the circuit server-side. Server-side compilers route but do not optimise, so gate count and depth can grow substantially — enough to exceed a device's gate limit. A job that fails with `VALIDATION_FAILED` and a gate count limit error is this case; resubmit it in native mode. Native mode routes and optimises the circuit locally, lowers it to the device's native gates, validates gate angles against the device's restrictions (e.g. Rigetti runs `rx` only at ±π/2 and ±π — violations raise `Angle ... is not supported`), and submits it in a Braket verbatim box: ```python job = backend.run(circuit, shots=100, native=True, optimization_level=3) ``` An `optimization_level` of 2 or 3 is recommended, and one of `optimization_level` or `pass_manager` is required. Both require `native=True` and cannot be combined with each other or with `verbatim=True`. Verbatim mode submits a circuit unchanged, and AWS rejects the task if it contains any non-native gate: ```python native_circuit = transpile(circuit, backend) job = backend.run(native_circuit, shots=100, verbatim=True) ``` > \[!TIP] > Prefer native mode over verbatim mode when you want native-gate execution. > It derives the gate set and angle restrictions from the device itself, which transpiling against the backend does not guarantee. #### Retrieving Quantum Jobs Due to queuing at the quantum provider, job execution may take hours or even days. To retrieve your job later, you can use the `retrieve_job` function provided by the backend: ```python{6} provider = HubQiskitProvider() backend = provider.get_backend("kipu.sim.qsim") # Retrieve the job through its id job = backend.retrieve_job("6ac422ad-c854-4af4-b37a-efabb159d92e") ``` You can also get an overview of all your jobs by executing `provider.jobs()` or by visiting the [Quantum Jobs](https://dashboard.hub.kipu-quantum.com/quantum-jobs) page. ### Using HubQiskitRuntimeService The `HubQiskitRuntimeService` provides IBM [QiskitRuntimeService](https://quantum.cloud.ibm.com/docs/en/api/qiskit-ibm-runtime/qiskit-runtime-service) compatible API for running quantum circuits with sessions on IBM backends provided via Kipu Quantum Hub. > \[!IMPORTANT] > The `HubQiskitRuntimeService` only supports IBM quantum backends available through Kipu Quantum Hub. For other providers (Azure, AWS, etc.), use the `HubQiskitProvider` instead. Import the service and instantiate it: ```python from qhub.quantum.sdk import HubQiskitRuntimeService ``` #### Authentication Authentication works the same way as with `HubQiskitProvider` - either through CLI login or by providing access tokens and organization IDs as parameters. #### Using Sessions with IBM Backends The main advantage of `HubQiskitRuntimeService` is its support for Qiskit sessions, which allow you to run multiple circuits with shared context on IBM quantum backends: ```python from qhub.quantum.sdk import HubQiskitRuntimeService from qiskit import QuantumCircuit, generate_preset_pass_manager from qiskit_ibm_runtime import Session from qiskit_ibm_runtime import SamplerV2 as Sampler # Create a quantum circuit bell_circuit = QuantumCircuit(2) bell_circuit.h(0) bell_circuit.cx(0, 1) bell_circuit.measure_all() # Initialize the service and get an IBM backend service = HubQiskitRuntimeService() backend = service.backend("ibm.qpu.aachen") # Example IBM backend # Transpile the circuit for the target backend pm = generate_preset_pass_manager(backend=backend, optimization_level=1) isa_circuit = pm.run(bell_circuit) # Execute the circuit using a session with Session(backend=backend) as session: sampler = Sampler(mode=session) job = sampler.run([isa_circuit], shots=1000) print(f"Job ID: {job.job_id()}") result = job.result() print(f"Counts: {result[0].data.meas.get_counts()}") ``` #### Retrieving Quantum Jobs To retrieve your job, you can use the `job` function provided by the service: ```python service = HubQiskitRuntimeService() # Retrieve the job through its id job = service.job("1a6fe637-bbdd-4b91-8b52-068e43be69b7") print(f"Status: {job.status()}") if job.done(): result = job.result() print(f"Result: {result}") ``` #### Bring Your Own IBM Token Use `HubQiskitRuntimeService` only when accessing IBM backends **through Kipu Quantum Hub** (using Hub-managed access). If you want to run on IBM using **your own IBM token** (bring your own token), do **not** use `HubQiskitRuntimeService`. Instead, use IBM's plain [`QiskitRuntimeService`](https://quantum.cloud.ibm.com/docs/en/api/qiskit-ibm-runtime/qiskit-runtime-service) from the `qiskit-ibm-runtime` package directly. Add your IBM token as a [Provider Access Token](manage-access-tokens#provider-access-tokens) and enable *Add secrets to runtime environment* so the platform injects `QISKIT_IBM_TOKEN` (and, for IBM Cloud, `QISKIT_IBM_INSTANCE` and `QISKIT_IBM_CHANNEL`) at runtime. See the [Use Qiskit Runtime in a Service](tutorials/tutorial-qiskit-runtime) tutorial for the full flow. ## Using Amazon Braket In your Python code you can access the Kipu Quantum Hub quantum backends through the `HubBraketProvider` class. We refer to these backends as *devices* in the following to adhere to the Braket SDK naming conventions. Import the class and instantiate it as shown below: ```python from qhub.quantum.sdk import HubBraketProvider ``` If you are already logged in with [qhubctl](quickstart#login-to-your-account) you can create the provider object without any parameters: ```python provider = HubBraketProvider() ``` Alternatively, you can also create the provider object by passing your Kipu Quantum Hub [personal access token](manage-access-tokens#personal-access-tokens): ```python provider = HubBraketProvider(access_token="YOUR_PERSONAL_ACCESS_TOKEN_HERE") ``` If you want to log in with your organization, you can additionally pass the organization id as a parameter. The organization id can be found in the organization settings on the platform: ```python provider = HubBraketProvider(organization_id="YOUR_ORGANIZATION_ID_HERE", access_token="...") ``` ### Use the Provider Class After you have created the provider object, you can list all devices (backends) provided by Kipu Quantum Hub that can be accessed through Braket. ```python # List all available quantum devices devices = provider.devices() # Select a certain device device = provider.get_device("aws.ionq.forte") ``` > \[!TIP] > To access other QPUs, either you or your organization must have payment information added to your account. > To upgrade your account, go to your [Account Settings](https://dashboard.hub.kipu-quantum.com/settings/account), click the > *Upgrade* button, and follow the prompts to enter your payment details. ### Working with Braket Devices Now you can execute your Braket circuit on the selected device, retrieve its `task` object, retrieve its results, cancel it etc. The full example would look like this: ```python from braket.circuits import Circuit from qhub.quantum.sdk import HubBraketProvider from qhub.quantum.sdk.braket import HubAwsQuantumTask # Select the IonQ Forte device device = HubBraketProvider().get_device("aws.ionq.forte") # Create a Braket circuit circuit = Circuit().h(0).cnot(0, 1).cnot(1,2) # Execute the circuit with 100 shots task = device.run(circuit, 100) # Monitor task status and get results print(f"Status: {task.state()}) print(f"Result: {task.result()}) ``` To execute a task on the QuEra Aquila device, you'll need to create an [Analog Hamiltonian Simulation (AHS) program](https://github.com/amazon-braket/amazon-braket-examples/blob/main/examples/analog_hamiltonian_simulation/01_Introduction_to_Aquila.ipynb) and discretize it according to the device specifications. This is described in detail using the Maximum Independent Set Problem in our [Quera Aquila tutorial](./tutorials/tutorial-quera-mis). #### Retrieving Braket Tasks To retrieve a task you ran earlier, note down its ID and create a HubAwsQuantumTask object by providing the ID. Optionally, you can also provide an access token and an organization id. ```python # Submit the program to the device task = device.run(circuit, 100) # Get the task ID for future reference print("Task ID:", task.id) # Example Output: Task ID: 6ac422ad-c854-4af4-b37a-efabb159d92e # Retrieve the task using its ID task = HubAwsQuantumTask("6ac422ad-c854-4af4-b37a-efabb159d92e") ``` You can also get an overview of your tasks by visiting the [Quantum Jobs](https://dashboard.hub.kipu-quantum.com/quantum-jobs) page. Note that your tasks are referred to as “jobs” on this page. ## Supported Operations This section provides an overview of the most important classes and methods in the SDK. ### HubQiskitProvider The `HubQiskitProvider` class allows access to all gate-based backends via Qiskit. | Method | Description | |---------------------------|--------------------------------------------------------------------------------------------------------------------------------------| | `backends(detailed=False)`| Returns a list of backend IDs supported by Kipu Quantum Hub. When `detailed=True`, returns `BackendInfo` objects with SDK compatibility information. | | `get_backend(backend_id)` | This method returns a single backend that matches the specified ID. If the backend cannot be found, a `HubError` is thrown. | | `jobs()` | This method retrieves a list of all jobs created by the user, sorted by their creation date with the newest jobs listed first. | If you specify `kipu.sim.qsim` as the backend ID, for example, by calling `provider.get_backend("kipu.sim.qsim")`, a [`HubQiskitBackend`](#qiskit-backends-and-jobs) is returned. #### Discovering Provider Support To programmatically determine which provider supports a specific backend, use `backends(detailed=True)`: ```python backend_infos = provider.backends(detailed=True) for info in backend_infos: print(f"{info.id}: {info.supported_providers}") # Example output: # kipu.sim.qsim: {'HubQiskitProvider'} # aws.ionq.aria: {'HubQiskitProvider', 'HubBraketProvider'} # ibm.qpu.aachen: {'HubQiskitRuntimeService'} # Check if a specific backend supports a particular provider if backend_infos[0].supports_provider("HubBraketProvider"): print(f"{backend_infos[0].id} can be accessed via HubBraketProvider") ``` The `supported_providers` field shows which provider classes can access each backend (see [provider overview](#accessing-quantum-backends)). ### Qiskit Backends and Jobs The `HubQiskitBackend` class represents a [Qiskit Backend](https://qiskit.org/documentation/stubs/qiskit.providers.BackendV2.html). It provides information about quantum backends (e.g., number of qubits, qubit connectivity, etc.) and enables you to run quantum circuits on the backend. Please note that currently, only circuits with gate-based operations are supported while pulse-based operations are not supported. The `HubQiskitBackend` class supports the following methods: | Method | Description | |------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `configuration()` | Returns the backend configuration data. This method is included for compatibility with older versions of Qiskit. | | `run(circuit, shots)` | Executes a single circuit on the backend as a job (multiple circuits are currently not supported) and returns a `HubQiskitJob`. You also need to specify the number of shots. The minimum and maximum number of supported shots differ for each backend and can be obtained from the backend properties `min_shots` and `max_shots`, respectively. A `HubError` is thrown if the job input is invalid or if the designated backend is offline and does not accept new jobs in the moment. | | | `retrieve_job(job_id)` | Retrieves a job from the backend using the provided id. If a job cannot be found a `HubError` is thrown. | | This example shows how to run a circuit on a backend: ```python # Select a certain backend backend = provider.get_backend("kipu.sim.qsim") # Create a circuit circuit = QuantumCircuit(2, 2) circuit.h(0) circuit.cx(0, 1) circuit.measure(range(2), range(2)) # Run the circuit on the backend job = backend.run(circuit, shots=10) # Retrieve a job by id job = backend.retrieve_job("6ac422ad-c854-4af4-b37a-efabb159d92e") ``` #### Qiskit Jobs & Results The class `HubQiskitJob` represents a [Qiskit Job](https://qiskit.org/documentation/stubs/qiskit.providers.JobV1.html#jobv1). It provides status information about a job (e.g., job id, status, etc.) and enables you to access the job result as soon as the job execution has completed successfully. ##### Methods | Method | Description | |------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `status()` | Returns the status of the job. The [Qiskit job states](https://qiskit.org/documentation/stubs/qiskit.providers.JobStatus.html) are: `INITIALIZING`, `QUEUED`, `RUNNING`, `CANCELLED`, `DONE`, `ERROR`. | | `result()` | Returns the result of the job. It blocks until the job execution has completed successfully. If the job execution has failed, a `HubError` is thrown indicating that the job result is not available. | | `cancel()` | Cancels the job execution. If the job execution has already completed or if it has failed, this method has no effect. | ##### Results The type of result depends on the backend where the job was executed. Currently, only measurement result histograms are supported. The histogram is represented as a dictionary where the keys are the measured qubit states and the values are the number of occurrences. The measured qubit states are represented as bit-strings where the qubit farthest to the right is the most significant and has the highest index (little-endian). If supported by the backend, the result also contains the memory of the job execution, i.e., the qubit state of each individual shot. ##### Attributes | Attribute | Description | |-----------|---------------------------------------------------------| | `counts` | Returns the histogram of the job result as a JSON dict. | | `memory` | Returns the memory as a JSON dict. | Here is an example of how to access these attributes: ```python result = job.result() print(result.counts) # Expected output, e.g., {"11": 6, "00": 4} print(result.memory) # Expected output, e.g., ['00', '11', '11', '00', '11', '00', '11', '11', '00', '11'] ``` ### HubQiskitRuntimeService The `HubQiskitRuntimeService` class provides IBM Qiskit Runtime compatible API for session-based quantum computing on IBM backends only. | Method | Description | |---------------------------|--------------------------------------------------------------------------------------------------------------------------------------| | `backend(name)` | Returns a single IBM backend that matches the specified ID. Only IBM backends are supported for runtime operations. | | `backends()` | Returns a list of registered IBM backend IDs available for runtime operations. | | `job(job_id)` | Retrieve a runtime job by its ID. Returns a `HubRuntimeJobV2` instance. | The service is designed to be compatible with IBM's Qiskit Runtime patterns, enabling the use of sessions, primitives (Sampler, Estimator), and other runtime features. #### Runtime Jobs Jobs created by `HubQiskitRuntimeService` are of type `HubRuntimeJobV2`, which provides full compatibility with IBM's Runtime job interface: | Method | Description | |---------------------|---------------------------------------------------------------------------------------------| | `status()` | Returns the job status (`INITIALIZING`, `QUEUED`, `RUNNING`, `DONE`, `ERROR`, `CANCELLED`) | | `result()` | Returns the decoded job results as a [`PrimitiveResult`](https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.primitives.PrimitiveResult). Blocks until job completion. | | `cancel()` | Cancels the job execution if still in progress. | The job result format depends on the runtime primitive that was used: * **SamplerV2**: Results contain measurement counts and bitstrings accessible, e.g., via `result[0].data.meas.get_counts()` * **EstimatorV2**: Results contain expectation values for observables accessible, e.g., via `result[0].data.evs` ### HubBraketProvider The `HubBraketProvider` class allows access to all backends provided through AWS. This is an overview of the available methods: | Method | Description | |-------------------------|-------------------------------------------------------------------------------------------------------------------------------------| | `devices()` | Returns a list of device IDs supported by Kipu Quantum Hub through Braket. | | `get_device(backend_id)` | This method returns a single device that matches the specified ID. If the backend cannot be found, a `HubError` is thrown. | If you specify `aws.ionq.forte` as the backend ID, for example, by calling `provider.get_device("aws.ionq.forte")`, a `HubAwsDevice` is returned. ### Braket Devices and Tasks The `HubAwsDevice` class represents an [`AwsDevice`](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/src/braket/aws/aws_device.py) and therefore provides the same properties and methods. Below are the key methods and properties: | Property / Method | Description | |----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `status` | Retrieves the current status of the device. | | `is_available` | Returns `true` if the device is online and ready to process tasks. | | `properties` | Provides the current properties of the device. | | `run(task_specification, shots)` | Executes a Braket circuit or an Analog Hamiltonian Simulation program on Kipu Quantum Hub (batch executions are not currently supported) and returns a `HubAwsQuantumTask`. You can specify the number of shots to perform; if not specified, 1000 shots are executed by default. A `HubError` is thrown if the task input is invalid or if the device is offline and unable to accept new jobs. | #### Tasks & Results The `HubAwsQuantumTask` class is a representation of an [AwsQuantumTask](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/src/braket/aws/aws_quantum_task.py). This class provides essential status information about a task, such as its ID, current status, and allows access to its results once the execution is completed successfully. You can obtain a `HubAwsQuantumTask` object directly from the `run` function of the `HubAwsDevice`. Alternatively, if you need to retrieve a task later, you can create a `HubAwsQuantumTask` object by specifying the task ID. For example, to retrieve a task with the ID `123e4567-e89b-42d3-a456-556642440000`, you would use: ```python task = HubAwsQuantumTask(task_id="123e4567-e89b-42d3-a456-556642440000") ``` If you are not logged in through qhubctl, you must also provide your access token, and optionally, your organization ID. ```python HubAwsQuantumTask(task_id="123e4567-e89b...", access_token="your_access_token", organization_id="your_organization_id") ``` ##### Methods | Method | Description | |------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `state()` | Returns the current state of the task, which could be `QUEUED`, `RUNNING`, `CANCELLED`, `COMPLETED`, or `FAILED`. | | `result()` | Returns the execution result of the task. This method blocks until the task execution completes successfully. If the task fails, a `HubError` is thrown, indicating that the result is unavailable. | | `cancel()` | Cancels the task execution. If the task has already completed or failed, this method has no effect. | ##### Results If you execute a Braket circuit the result object is of type [ `GateModelQuantumTaskResult`](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/src/braket/tasks/gate_model_quantum_task_result.py). For [ `AnalogHamiltonianSimulationQuantumTaskResult`](https://github.com/amazon-braket/amazon-braket-sdk-python/blob/main/src/braket/tasks/analog_hamiltonian_simulation_quantum_task_result.py). Both result classes include the shot measurements from the execution. A `GateModelQuantumTaskResult` contains for instance the following properties: ```python result = task.result() print(result.measurement_counts) # Expected output, e.g., Counter({'111': 2, '000': 1}) print(result.measurements) # Expected output [[0 0 0][1 1 1][1 1 1]] ``` ## PennyLane Integration To use the SDK with PennyLane, you need to install the [PennyLane-Qiskit plugin](https://docs.pennylane.ai/projects/qiskit/en/latest) by adding the `pennylane-qiskit` package to your Python project dependencies, e.g., by running `pip install pennylane-qiskit==0.43.0`. ::: warning IMPORTANT Currently, only `pennylane` and `pennylane-qiskit` packages version 0.43.0 are supported. ::: To execute a PennyLane circuit using a Kipu Quantum Hub backend, first, retrieve the desired backend using the [HubQiskitProvider](#hubqiskitprovider).\ Then, create a `qiskit.remote` device and pass the backend to it. The following example shows how to create a remove device using the `kipu.sim.qsim` backend: ```python provider = HubQiskitProvider() backend = provider.get_backend("kipu.sim.qsim") device = qml.device('qiskit.remote', wires=2, backend=backend, shots=100) @qml.qnode(device) def circuit(): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) return qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliZ(1)) result = circuit() ``` ## What's next? * See our supported [quantum backends and simulators](https://hub.kipu-quantum.com/quantum-backends). * Checkout how to create your first [Service project](quickstart#create-your-first-service-project). --- --- url: /sdk-service.md description: >- Install and use the qhub-service SDK in Python or TypeScript to execute services, submit jobs, and retrieve results programmatically. --- # Kipu Quantum Hub Service SDK > \[!INFO] At a glance > > * **Purpose:** Programmatic client for Managed Services on Kipu Quantum Hub — submit executions, poll status, fetch results and logs, download result files, and attach short-lived data pool grants. > * **Packages:** [`qhub-service`](https://pypi.org/project/qhub-service) *(Python 3.9+, installed via `pip` or `uv`)* · [`@quantum-hub/qhub-service`](https://www.npmjs.com/package/@quantum-hub/qhub-service) *(TypeScript/JavaScript, installed via `npm`, `yarn`, or `pnpm`)* > * **Entry point:** `HubServiceClient` (Python and TypeScript share the same surface) > * **Use it when:** you want to drive a Managed Service from a script, notebook, or application — or automate an end-to-end pipeline around one. > * **Pairs with:** [`qhubctl login`](cli-reference.md) stores the personal access token the Python client auto-resolves for data pool grants. Service endpoints and Access Keys are configured in your [Service Settings](https://dashboard.hub.kipu-quantum.com/services). > * **Quick reference:** `HubServiceClient(endpoint, accessKeyId, secretAccessKey).run(request)` → `ServiceExecution.result()` / `.logs()` / `.cancel()` > * **New here?** See the [References Overview](references/index.md). The Kipu Quantum Hub Service SDK lets you interact programmatically with managed services on the [Kipu Quantum Hub](https://hub.kipu-quantum.com). It provides idiomatic clients in Python and TypeScript that wrap the underlying REST API to: * Submit service executions to a managed service. * Track execution status and wait for a final state. * Retrieve results, download result files, and stream logs. * Cancel pending or running executions. * Inject short-lived Data Pool access grants into a request. The SDK ships in two flavours that expose the same conceptual surface: | Language | Package | |------------|----------------------------------------------------------------------------------------| | Python | [`qhub-service`](https://pypi.org/project/qhub-service) | | TypeScript | [`@quantum-hub/qhub-service`](https://www.npmjs.com/package/@quantum-hub/qhub-service) | ## Installation Python requires `>= 3.9`. The TypeScript package is published as an ES module. ::: tabs key:pythonTS \== Python Install the SDK using `pip` or `uv`: ```bash uv add qhub-service # OR with native pip pip install --upgrade qhub-service ``` \== TypeScript Install the SDK using npm or yarn: ```bash npm install @quantum-hub/qhub-service # or yarn add @quantum-hub/qhub-service ``` ::: ## Concepts The SDK models the following resources: * **Service** — a managed application published on the Kipu Quantum Hub marketplace (e.g. *Rimay - Quantum Feature Extraction - Simulator*). A service exposes a gateway endpoint that clients call. * **Application** — a client-side container in your organization. An application owns access keys and groups subscriptions. * **Subscription** — a link between an application and a service. A subscription provides the `gatewayEndpoint` used for execution requests. * **Access key** — an `accessKeyId` / `secretAccessKey` pair issued per application. Used with the OAuth 2.0 client-credentials flow to obtain short-lived bearer tokens. * **Service execution** — a single invocation of a service. Identified by a UUID, progresses through a lifecycle of statuses (`PENDING` → `RUNNING` → `SUCCEEDED` / `FAILED` / `CANCELLED`). * **Data Pool** — a managed storage resource. Services that read from or write to a data pool require a short-lived JWT access *grant* scoped to `(datapoolId, applicationId, permission)`. ## Authentication The SDK uses two distinct authentication mechanisms: ### Service gateway (OAuth 2.0 client credentials) Calls to the service gateway are authenticated with a bearer token obtained via the OAuth 2.0 client-credentials flow using an application's `accessKeyId` and `secretAccessKey`. * The default token endpoint is `https://gateway.hub.kipu-quantum.com/token`. * The SDK caches the token and transparently refreshes it before it expires (Python refreshes ~120 seconds before expiry; TypeScript checks `AccessToken.expired()` before every request). * If both `accessKeyId` and `secretAccessKey` are omitted, the client falls back to a random token, which is useful for local gateways that do not enforce auth. ### Platform API (personal access token) Calls to the Hub Platform API (used for data pool grants and service discovery) are authenticated with a personal access token (API key). * In **Python**, credentials and the active organization are resolved automatically via `DefaultCredentialsProvider` and `ContextResolver`. If you are already logged in through the `qhubctl` CLI, no parameters need to be supplied. * In **TypeScript**, you must pass a `HubPlatformClient` with an explicit `apiKey`. ## Quickstart ::: tabs key:pythonTS \== Python ```python import os from qhub.service.client import HubServiceClient client = HubServiceClient( service_endpoint="https://gateway.hub.kipu-quantum.com/acme/qft-simulator/1.0.0", access_key_id=os.environ["ACCESS_KEY_ID"], secret_access_key=os.environ["SECRET_ACCESS_KEY"], ) # Start a new execution execution = client.run(request={"values": [2], "shots": 100}) # Block until the execution finishes execution.wait_for_final_state(timeout=300) print(f"Finished at {execution.ended_at} with status {execution.status}") # Fetch the result result = execution.result() result.data() # service-computed values, e.g. {"counts": {...}, "elapsed_time": 6.54} result.metadata() # execution metadata (a ServiceExecution: status, timestamps, ids) result.files() # downloadable result file names # Download every result file into the current working directory for file_name in result.files(): execution.download_result_file(file_name, os.getcwd()) ``` \== TypeScript ```typescript import {ExecutionResult, HubServiceClient} from '@quantum-hub/qhub-service' const client = new HubServiceClient( 'https://gateway.hub.kipu-quantum.com/acme/qft-simulator/1.0.0', process.env.ACCESS_KEY_ID, process.env.SECRET_ACCESS_KEY, ) // Start a new execution let execution = await client.run({values: [2], shots: 100}) // Poll the status until the execution reaches a final state const finalStates = ['SUCCEEDED', 'CANCELLED', 'FAILED'] while (!finalStates.includes(execution.status!)) { execution = await client.api().getStatus(execution.id!) } // TypeScript has no polling helper yet — reach a final state first, then fetch. const result = ExecutionResult.from(await client.api().getResult(execution.id!)) result.data() // service-computed values, e.g. {counts: {...}, elapsed_time: 6.54} result.metadata() // ServiceExecution | undefined; check result.metadata()?.status result.files() // downloadable result file names result.raw // the untouched ResultResponse ``` ::: ## HubServiceClient `HubServiceClient` is the primary entry point. It manages authentication, exposes the low-level REST API via `api`, and offers high-level helpers for submitting executions and acquiring data pool grants. ### Constructor ::: tabs key:pythonTS \== Python ```python HubServiceClient( service_endpoint: str, access_key_id: str | None, secret_access_key: str | None, token_endpoint: str = "https://gateway.hub.kipu-quantum.com/token", platform_client: HubPlatformClient | None = None, ) ``` \== TypeScript ```typescript new HubServiceClient( serviceEndpoint: string, accessKeyId?: string, secretAccessKey?: string, tokenEndpoint: string = 'https://gateway.hub.kipu-quantum.com/token', platformClient?: HubPlatformClient, ) ``` ::: | Parameter | Required | Description | |---------------------|----------|------------------------------------------------------------------------------------| | `service_endpoint` | yes | Gateway URL of the subscribed service. | | `access_key_id` | no\* | Application access key id. Omit for anonymous/local gateways. | | `secret_access_key` | no\* | Application secret access key. Omit for anonymous/local gateways. | | `token_endpoint` | no | OAuth 2.0 token endpoint. Defaults to the Kipu Quantum Hub gateway token endpoint. | | `platform_client` | no | Pre-configured `HubPlatformClient`. Required only when using data pool grants. | \* Both access key parameters must either be supplied together or both be omitted. In **Python**, if `platform_client` is not supplied the constructor attempts to create one from the ambient credentials (via `DefaultCredentialsProvider`). If no credentials are available the field is left as `None` and attempts to use grants will raise. ### `api` — low-level REST client `api` returns the generated REST client (`ServiceApiClient`) that exposes every endpoint of the service gateway directly. Use it when you need fine-grained control that the high-level helpers do not provide. ::: tabs key:pythonTS \== Python ```python client.api.get_service_executions() client.api.start_execution(request={"shots": 100}) client.api.get_status(id="...") client.api.get_result(id="...") client.api.get_result_file(id="...", file="output.json") # returns Iterator[bytes] client.api.get_logs(id="...") client.api.cancel_execution(id="...") ``` \== TypeScript ```typescript await client.api().getServiceExecutions() await client.api().startExecution({shots: 100}) await client.api().getStatus(id) await client.api().getResult(id) await client.api().getResultFile(id, 'output.json') await client.api().getLogs(id) await client.api().cancelExecution(id) ``` ::: ### `run` — submit a new execution Starts a new service execution. In Python, `run` returns a rich `HubServiceExecution` wrapper with polling and result helpers. In TypeScript, `run` returns the raw `ServiceExecution` DTO. ::: tabs key:pythonTS \== Python ```python run( request: dict[str, Any], secrets: dict[str, Any] | None = None, tags: list[str] | None = None, grants: list[DataPoolGrant] | None = None, ) -> HubServiceExecution ``` \== TypeScript ```typescript run( request: Record, secrets?: Record, tags?: string[], options?: {grants?: DataPoolGrant[]}, ): Promise ``` ::: Behaviour: * `tags` are injected under the reserved key `$tags` (unless the caller already set it). * `secrets` are injected under the reserved key `$secrets` (unless the caller already set it). * `grants` trigger acquisition of short-lived JWTs against the Platform API and inject `DataPoolReference` objects under the `name` of each grant (see [Data Pool Access Grants](#data-pool-access-grants)). Requires a configured `HubPlatformClient`. * Keys the caller already placed in the request are never overwritten by `$tags` or `$secrets`. Grants, by design, overwrite any pre-existing key with the same name so that the injected JWT always takes effect. ### Listing and fetching executions ::: tabs key:pythonTS \== Python ```python # Fetch a single execution by its ID (returns a HubServiceExecution wrapper) execution = client.get_service_execution("0030737b-35cb-46a8-88c2-f59d4885484d") # List every execution visible to the authenticated application for execution in client.get_service_executions(): print(execution.id, execution.status, execution.created_at) ``` \== TypeScript ```typescript // Fetch a single execution by its ID const execution = await client.api().getStatus( '0030737b-35cb-46a8-88c2-f59d4885484d', ) // List every execution visible to the authenticated application for (const e of await client.api().getServiceExecutions()) { console.log(e.id, e.status, e.createdAt) } ``` ::: ## Service Executions In Python, `HubServiceExecution` wraps a `ServiceExecution` DTO and provides convenience operations (refreshing status, waiting for a final state, downloading files). In TypeScript these helpers are not wrapped — use the raw `ServiceExecution` and the `api()` client directly. ### Lifecycle ``` PENDING ──▶ RUNNING ──▶ SUCCEEDED │ ├───────▶ FAILED │ └───────▶ CANCELLED ``` `UNKNOWN` is used when the gateway cannot report a status (generally only on transient errors). The final states are `SUCCEEDED`, `FAILED`, and `CANCELLED`. ### `HubServiceExecution` members (Python) | Member | Description | |-------------------------------------------|---------------------------------------------------------------------------------------------------------| | `id` | UUID of the execution. | | `status` | One of `UNKNOWN`, `PENDING`, `RUNNING`, `SUCCEEDED`, `CANCELLED`, `FAILED`. | | `created_at` | ISO-8601 timestamp of when the execution was created. | | `started_at` | ISO-8601 timestamp of when the execution transitioned to `RUNNING`. `None` until set. | | `ended_at` | ISO-8601 timestamp of when the execution reached a final state. `None` until set. | | `has_finished` | `True` once the status is a final state. Refreshes the status on every access. | | `refresh()` | Re-fetch the status from the gateway. | | `wait_for_final_state(timeout, wait)` | Poll until a final state is reached. Raises `TimeoutError` when `timeout` is exceeded. | | `result()` | Wait for the final state, then fetch and return the `Result` (Result data + metadata + files). Retries with exponential backoff. | | `result().data()` | The service-computed **Result data** as a `dict` (everything except `_links`/`_embedded`); `{}` when none. | | `result().metadata()` | The **Execution metadata** (`ServiceExecution`); read `.status` to check the final state. | | `result().files()` | The downloadable **Result file** names (same as `result_files()`). | | `result_files()` | List the names of downloadable result files (excludes the HAL `status` and `self` links). | | `result_file_stream(name)` | Return an iterator of `bytes` for the named result file. | | `download_result_file(name, target_path)` | Stream the named result file into the directory `target_path`. | | `cancel()` | Cancel the execution if still pending or running. | | `logs()` | Return the list of `LogEntry` items attached to the execution. | ### Waiting for a final state ::: tabs key:pythonTS \== Python `wait_for_final_state` blocks until the execution transitions to a final state or `timeout` seconds have elapsed. ```python # Wait indefinitely with a 5 second polling interval execution.wait_for_final_state() # Wait at most 5 minutes, polling every 10 seconds execution.wait_for_final_state(timeout=300, wait=10) ``` Raises `TimeoutError` if the deadline is reached before a final state. \== TypeScript There is no built-in helper — poll `getStatus` until the status is a final state. ```typescript const finalStates = ['SUCCEEDED', 'CANCELLED', 'FAILED'] let execution = await client.api().getStatus(id) while (!finalStates.includes(execution.status!)) { await new Promise((r) => setTimeout(r, 5_000)) execution = await client.api().getStatus(id) } ``` ::: ### Downloading result files ::: tabs key:pythonTS \== Python `result_files` returns the list of downloadable file names excluding the HAL navigational links (`status`, `self`). When you already hold a `Result` from `result()`, prefer `result.files()` — `result_files()` fetches the result again. ```python import os for name in execution.result_files(): execution.download_result_file(name, os.getcwd()) ``` `download_result_file` streams the file into the provided directory. The target **must exist and be a directory**; otherwise the method raises `ValueError`. For manual streaming, use `result_file_stream` which returns an iterator of chunks: ```python with open("result.json", "wb") as f: for chunk in execution.result_file_stream("result.json"): f.write(chunk) ``` \== TypeScript Use `ExecutionResult.files()` to get downloadable file names and `api().getResultFile()` to stream each file. ```typescript import {writeFile} from 'node:fs/promises' import {ExecutionResult} from '@quantum-hub/qhub-service' const result = ExecutionResult.from(await client.api().getResult(id)) for (const name of result.files()) { const binary = await client.api().getResultFile(id, name) await writeFile(name, Buffer.from(await binary.bytes())) } ``` ::: ### Reading logs ::: tabs key:pythonTS \== Python ```python logs = execution.logs() or [] logs.sort(key=lambda entry: entry.timestamp) for entry in logs: print(entry.severity, entry.timestamp, entry.message) ``` \== TypeScript ```typescript const logs = (await client.api().getLogs(id)) ?? [] logs.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()) for (const entry of logs) { console.log(entry.severity, entry.timestamp, entry.message) } ``` ::: ### Cancelling an execution ::: tabs key:pythonTS \== Python ```python execution.cancel() ``` \== TypeScript ```typescript await client.api().cancelExecution(id) ``` ::: ## Data Pool Access Grants A service run is checked against the **application** it executes under — specifically the identity that **owns** that application — not against you, the end user who triggered it. When the pool you reference is held by a **different** identity than the application's owner, that owner has no permission on the pool and the run is denied — even though you do have access. This happens, for example, when an application owned by your **organization** reads a data pool that only you, or you and a colleague through a share, can access. The only previous workaround was to share the pool with the application owner's entire organization — which exposes it to people who should never see it. A grant replaces that. For a single run, you — who do have access — authorize one specific application to read or write one specific pool, and nothing else. You never share the pool or change who can access it. Concretely, the grant is a short-lived JWT that the SDK acquires for you by calling the Hub Platform API, then attaches to the request. ### When you need one You need a grant only when the application your run executes under cannot reach the pool on its own: * An application owned by your **organization** reads a pool you hold **personally**. * A pool that was **shared** with you is read by an application owned by a different identity. You don't need a grant when the application and the pool share the same owner — for example your personal application reading your own pool, or your organization's application reading your organization's pool. Those runs are authorized automatically. ### Data model * `DataPoolPermission` — `VIEW` or `MODIFY`. * `DataPoolReference` — the in-request representation of a data pool. Has a stable `ref` field set to `DATAPOOL`, the `id` of the pool, and an optional `grant` JWT. * `DataPoolGrant` — the grant specification passed to `run`: * `name` — the key under which the resolved `DataPoolReference` will be injected. * `datapool_id` / `datapoolId` — the id of the data pool to reference. * `application_id` / `applicationId` — the application under which the grant is requested. * `permission` — defaults to `MODIFY`. ### Example ::: tabs key:pythonTS \== Python ```python from qhub.service.client import HubServiceClient from qhub.service.datapool import DataPoolGrant, DataPoolPermission # Credentials and organization are resolved via qhubctl / the platform context. client = HubServiceClient(service_endpoint, access_key_id, secret_access_key) # application_id is the application the run executes under (the one being authorized). grants = [ DataPoolGrant( name="input_data", datapool_id="dp-abc123", application_id="app-xyz789", permission=DataPoolPermission.VIEW, ), DataPoolGrant( name="output_data", datapool_id="dp-def456", application_id="app-xyz789", permission=DataPoolPermission.MODIFY, ), ] execution = client.run(request={"shots": 100}, grants=grants) ``` To override the resolved credentials or organization pass an explicit `HubPlatformClient`: ```python from qhub.service.client import HubServiceClient, HubPlatformClient platform_client = HubPlatformClient(api_key="...", organization_id="...") client = HubServiceClient( service_endpoint, access_key_id, secret_access_key, platform_client=platform_client, ) ``` \== TypeScript ```typescript import {HubServiceClient, HubPlatformClient} from '@quantum-hub/qhub-service' import { DataPoolPermission, type DataPoolGrant, } from '@quantum-hub/qhub-service/datapool' const platformClient = new HubPlatformClient({ apiKey: '...', organizationId: '...', }) const client = new HubServiceClient( serviceEndpoint, accessKeyId, secretAccessKey, undefined, platformClient, ) // applicationId is the application the run executes under (the one being authorized). const grants: DataPoolGrant[] = [ { name: 'input_data', datapoolId: 'dp-abc123', applicationId: 'app-xyz789', permission: DataPoolPermission.VIEW, }, { name: 'output_data', datapoolId: 'dp-def456', applicationId: 'app-xyz789', permission: DataPoolPermission.MODIFY, }, ] const execution = await client.run({shots: 100}, undefined, undefined, {grants}) ``` ::: ### Caching Grants are cached per `(datapoolId, applicationId, permission)` tuple until 30 seconds before the issued JWT's `exp` claim. Re-running an execution with the same grant re-uses the cached token, which keeps the number of platform calls bounded even when executions are issued in a tight loop. If `run` is called with `grants` but no `HubPlatformClient` is available the call fails fast: * Python raises `ValueError: api_key is required for data pool grant acquisition`. * TypeScript throws `Error: apiKey is required for data pool grant acquisition`. ### Manual references If your workflow already owns a valid grant token you can bypass automatic acquisition and place a `DataPoolReference` directly in the request: ::: tabs key:pythonTS \== Python ```python from qhub.service.datapool import DataPoolReference request = { "input_data": DataPoolReference(id="dp-abc123", grant=my_jwt).dict(), } client.run(request=request) ``` \== TypeScript ```typescript import {ReferenceType} from '@quantum-hub/qhub-service/datapool' await client.run({ input_data: { id: 'dp-abc123', ref: ReferenceType.DATAPOOL, grant: myJwt, }, }) ``` ::: When both a manual reference and a grant for the same key are supplied, the grant takes precedence and overwrites the manual entry. ## HubPlatformClient A thin wrapper around the Platform API used by `HubServiceClient` for data pool grants. ::: tabs key:pythonTS \== Python ```python from qhub.service.client import HubPlatformClient platform = HubPlatformClient( api_key="...", # optional - resolved via DefaultCredentialsProvider organization_id="...", # optional - resolved via ContextResolver platform_endpoint="https://api.hub.kipu-quantum.com/qc-catalog", ) platform.organization_id platform.data_pool_grants # generated client for the /data-pool-grants API ``` \== TypeScript ```typescript import {HubPlatformClient} from '@quantum-hub/qhub-service' const platform = new HubPlatformClient({ apiKey: '...', // required organizationId: '...', // optional platformEndpoint: 'https://api.hub.kipu-quantum.com/qc-catalog', // optional }) platform.organizationId platform.dataPoolGrants ``` ::: The TypeScript variant does **not** auto-resolve credentials — `apiKey` must be supplied explicitly. ## Advanced usage ### Batch processing Run multiple executions in parallel when you have independent inputs. Python uses a thread pool because the underlying HTTP client is synchronous; TypeScript uses `Promise.all` since the client is already asynchronous. ::: tabs key:pythonTS \== Python ```python from concurrent.futures import ThreadPoolExecutor def process_batch(batch_data): execution = client.run(request={"data": batch_data, "params": {"mode": "batch"}}) execution.wait_for_final_state() return execution.result() batches = [ {"values": list(range(0, 100))}, {"values": list(range(100, 200))}, {"values": list(range(200, 300))}, ] with ThreadPoolExecutor(max_workers=3) as pool: results = list(pool.map(process_batch, batches)) print(f"Processed {len(results)} batches") ``` \== TypeScript ```typescript async function processBatch( client: HubServiceClient, batchData: Record, ): Promise { const execution = await client.run({data: batchData, params: {mode: 'batch'}}) const finalStates = ['SUCCEEDED', 'FAILED', 'CANCELLED'] let current = execution while (!finalStates.includes(current.status!)) { await new Promise((r) => setTimeout(r, 5_000)) current = await client.api().getStatus(current.id!) } return await client.api().getResult(current.id!) } const batches = [ {values: Array.from({length: 100}, (_, i) => i)}, {values: Array.from({length: 100}, (_, i) => i + 100)}, {values: Array.from({length: 100}, (_, i) => i + 200)}, ] const results = await Promise.all(batches.map((b) => processBatch(client, b))) console.log(`Processed ${results.length} batches`) ``` ::: ### Custom polling strategy Observe every status transition while you wait — useful for logging, progress UI, or emitting events without blocking on `wait_for_final_state`. ::: tabs key:pythonTS \== Python ```python import time def wait_with_progress(execution, timeout=None): start_time = time.time() last_status = None while not execution.has_finished: if execution.status != last_status: elapsed = time.time() - start_time print(f"[{elapsed:.1f}s] Status changed to: {execution.status}") last_status = execution.status if timeout and (time.time() - start_time) > timeout: raise TimeoutError("Execution timed out") time.sleep(5) print(f"Execution completed with status: {execution.status}") execution = client.run(request={"data": data, "params": params}) wait_with_progress(execution, timeout=300) ``` `has_finished` refreshes the status from the gateway on every access, so the loop does not need an explicit `refresh()` call. \== TypeScript ```typescript import type {HubService} from '@quantum-hub/qhub-api/service' async function waitWithProgress( client: HubServiceClient, executionId: string, timeoutMs: number = 300_000, ): Promise { const startTime = Date.now() const finalStates = ['SUCCEEDED', 'FAILED', 'CANCELLED'] let lastStatus: string | undefined while (Date.now() - startTime < timeoutMs) { const execution = await client.api().getStatus(executionId) if (execution.status !== lastStatus) { const elapsed = (Date.now() - startTime) / 1000 console.log(`[${elapsed.toFixed(1)}s] Status changed to: ${execution.status}`) lastStatus = execution.status } if (finalStates.includes(execution.status!)) { console.log(`Execution completed with status: ${execution.status}`) return execution } await new Promise((r) => setTimeout(r, 5_000)) } throw new Error('Execution timed out') } const execution = await client.api().startExecution({data, params}) await waitWithProgress(client, execution.id!) ``` ::: ## Reference ### DTOs #### `ServiceExecution` | Field | Type | Description | |-------------------------------------------------|--------------------------|---------------------------------------------| | `id` | `string` | Unique identifier of the service execution. | | `status` | `ServiceExecutionStatus` | Current status. | | `type` | `ServiceExecutionType?` | `MANAGED` or `WORKFLOW`. | | `created_at` / `createdAt` | `string` | ISO-8601 creation timestamp. | | `started_at` / `startedAt` | `string?` | ISO-8601 timestamp the execution started. | | `ended_at` / `endedAt` | `string?` | ISO-8601 timestamp the execution ended. | | `service_id` / `serviceId` | `string?` | ID of the underlying service. | | `service_definition_id` / `serviceDefinitionId` | `string?` | ID of the service definition. | | `application_id` / `applicationId` | `string?` | ID of the requesting application. | | `tags` | `string[]?` | Tags attached to the execution. | #### `ServiceExecutionStatus` `"UNKNOWN" | "PENDING" | "RUNNING" | "SUCCEEDED" | "CANCELLED" | "FAILED"` #### `ResultResponse` `execution.result()` (Python) returns a `Result` — a `ResultResponse` enriched with structured accessors. In TypeScript, wrap the raw response with `ExecutionResult.from(...)` to get the same accessors. The HAL envelope carries only `_links` and `_embedded`; every other top-level field is service-computed **Result data**. | Field / accessor | Type | Description | |--------------------------|------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------| | `links` / `_links` | `{ status?: HalLink, [rel: string]: HalLink }` | HAL links keyed by relation. Entries with keys `status` or `self` are navigational; everything else is a downloadable file. | | `embedded` / `_embedded` | `{ status?: ServiceExecution }` | The embedded execution resource. | | `data()` | `dict` / `Record` | The service-computed values (e.g. `counts`, `elapsed_time`) — every top-level field except `_links`/`_embedded`. Empty when none. | | `metadata()` | `ServiceExecution?` | Shortcut for `_embedded.status`; carries the execution `status`. | | `files()` | `string[]` | Downloadable result file names (excludes the navigational `status`/`self` links). | | `[""]` (Python) / `raw` (TS) | `Any` / `ResultResponse` | Raw access to service output. Python: a single field by key, collision-proof. TypeScript: the untouched response via `result.raw`. | In Python, service-computed fields are also accessible directly (e.g. `result.counts`); the `data()`/`metadata()`/`files()` accessors give a structured view that separates Result data from metadata and files. The names `data`, `metadata`, and `files` are reserved by the accessors; a service-output field with one of those names is reached by key, e.g. `result["data"]`. #### `LogEntry` | Field | Type | Description | |-------------|---------------------|-------------------------------------------------------| | `message` | `string` | Log message content. | | `severity` | `LogEntrySeverity?` | One of `DEBUG`, `NOTICE`, `INFO`, `WARNING`, `ERROR`. | | `timestamp` | `datetime` | When the entry was recorded. | #### `DataPoolReference` | Field | Type | Description | |---------|--------------|-------------------------------------------------------| | `id` | `string` | ID of the data pool. | | `ref` | `"DATAPOOL"` | Discriminator, always `DATAPOOL`. | | `grant` | `string?` | Optional short-lived JWT granting access to the pool. | #### `DataPoolGrant` | Field | Type | Description | |------------------------------------|-----------------------|-------------------------------------------------------------| | `name` | `string` | Key under which the injected `DataPoolReference` is placed. | | `datapool_id` / `datapoolId` | `string` | ID of the data pool to reference. | | `application_id` / `applicationId` | `string` | ID of the application requesting the grant. | | `permission` | `DataPoolPermission?` | `VIEW` or `MODIFY`. Defaults to `MODIFY`. | ### Reserved request keys When building the payload for `run` the SDK reserves two top-level keys for metadata: | Key | Purpose | |------------|--------------------------------------------| | `$tags` | List of tags attached to the execution. | | `$secrets` | Map of secret values resolved server-side. | If the caller already supplies either key, the corresponding keyword argument to `run` is ignored — caller-provided metadata wins. Data pool grants take precedence over any pre-existing value at the same key. ## Error Handling * **Bad credentials** to the service gateway result in the OAuth client raising at token time. In Python, exceptions from `authlib` propagate out of the first API call; in TypeScript, `simple-oauth2` throws on `getToken`. * **Missing data pool grant prerequisites:** calling `run` with `grants` but no `HubPlatformClient` raises `ValueError: api_key is required for data pool grant acquisition` in Python and throws the same message in TypeScript. * **`result()` after failure:** The Python helper retries the result fetch with exponential backoff (1s, 2s, 4s, 8s) before propagating the last exception. If the execution itself ended in `FAILED`, the returned result response reflects that status; consult `logs()` for diagnostics. * **`download_result_file`** raises `ValueError` if the target path is missing or is not a directory. * **`wait_for_final_state`** raises `TimeoutError` if the execution does not reach a final state before `timeout` elapses. ## Endpoints The SDK targets the following Kipu Quantum Hub endpoints by default. Each can be overridden via the corresponding constructor argument. | Endpoint | Default | Overridable via | |-----------------|--------------------------------------------------|-----------------------------------------------------------------| | Service gateway | Supplied per-subscription via `service_endpoint` | `HubServiceClient(service_endpoint=...)` | | OAuth token | `https://gateway.hub.kipu-quantum.com/token` | `HubServiceClient(token_endpoint=...)` | | Platform API | `https://api.hub.kipu-quantum.com/qc-catalog` | `HubPlatformClient(platform_endpoint=...)` / `platformEndpoint` | --- --- url: /sdk-api.md description: >- Multi-language clients for the Kipu Quantum Hub REST APIs - submit quantum jobs and manage sessions, list and inspect backends, catalog and share services and data pools, and execute managed services. --- # Kipu Quantum Hub API SDK > \[!INFO] At a glance > > * **Purpose:** multi-language clients for the Kipu Quantum Hub REST APIs - submit quantum jobs and manage sessions, list and inspect backends, catalog and share services and data pools, execute managed services, and read user profiles. > * **Packages:** [`qhub-api`](https://pypi.org/project/qhub-api/) (Python ≥ 3.9, `pip install qhub-api`), [`@quantum-hub/qhub-api`](https://www.npmjs.com/package/@quantum-hub/qhub-api) (Node ≥ 18 / ESM, `npm install @quantum-hub/qhub-api`). > * **Entry points:** `HubQuantumClient`, `HubPlatformClient`, `HubServiceClient`, `HubUserClient` - one per API surface, identical class names in both languages. > * **Use it when:** you need to talk to the Quantum Hub programmatically instead of through the dashboard or `qhubctl` CLI - for example, orchestrating jobs from a notebook, wiring the Hub into an application, or scripting catalog management. > * **Pairs with:** the `qhubctl` CLI (shares the same config file and env vars), the [Quantum Hub dashboard](https://hub.kipu-quantum.com), and the public [docs site](https://docs.hub.kipu-quantum.com). > * **Quick reference:** `HubQuantumClient(api_key=...).jobs.create_job(backend_id, shots, input) -> Job`, then `jobs.get_job_status(id)` / `jobs.get_job_result(id)`. > * **New here?** Start with [Installation](#installation), then jump to the [client](#the-sdk-clients) you need. The SDK wraps four independent APIs that together make up the Kipu Quantum Hub. Each API has its own client class because the four surfaces have distinct auth flows, base URLs, and scopes - you pick the client that matches the work you are doing. Capabilities exposed by the SDK: * Submit quantum jobs to any supported backend and stream their results. * Open sessions for batched or dedicated runs and drive them to completion. * List and inspect backends, including calibration, configuration, and the least-busy backend for a provider. * Browse the service catalog, manage service definitions, share services and data pools, and trigger managed or workflow service executions. * Read organization, subscription, application, data-pool, grant, billing, and notification resources on the platform. * Invoke deployed services through the service gateway using short-lived bearer tokens. * Read user profiles, manage personal access tokens, and resolve the currently-authenticated user. Packages shipped: | Package | Language | Install | |--------------------------------------------------------------------------------|-------------------------|-------------------------------------| | [`qhub-api`](https://pypi.org/project/qhub-api/) | Python | `pip install qhub-api` | | [`@quantum-hub/qhub-api`](https://www.npmjs.com/package/@quantum-hub/qhub-api) | TypeScript / JavaScript | `npm install @quantum-hub/qhub-api` | Both packages are generated from the same Fern/OpenAPI definitions and ship the same four clients and the same DTOs. Field casing differs per API (see each client's `Reference` section). ## Installation The Python package targets Python ≥ 3.9 and depends on `httpx` and `pydantic`. The TypeScript package is published as an ESM-only module with native-`fetch` transport, so it works on Node ≥ 18 and modern browsers. ::: tabs key:pythonTS \== Python ```bash pip install --upgrade qhub-api ``` \== TypeScript ```bash npm install @quantum-hub/qhub-api ``` ::: The TypeScript package exposes one entry point per API via subpath exports. Imports are always `@quantum-hub/qhub-api/quantum`, `@quantum-hub/qhub-api/platform`, `@quantum-hub/qhub-api/service`, or `@quantum-hub/qhub-api/user`; there is no top-level bundle export. ## The SDK clients The reference is split into one page per client. Pick the client that matches the API surface you are working with; the shared install, credential, and environment-variable material below applies to all four. | Client | Page | Auth | Use it for | | ------ | ---- | ---- | ---------- | | `HubQuantumClient` | [Quantum API](./sdk-api-quantum.md) | API key (`X-Auth-Token`) | Submit jobs, drive sessions, inspect backends. | | `HubPlatformClient` | [Platform API](./sdk-api-platform.md) | API key (`X-Auth-Token`) | Catalog: services, applications, organizations, data pools, billing. | | `HubServiceClient` | [Service API](./sdk-api-service.md) | Bearer token | Invoke deployed services through the gateway. | | `HubUserClient` | [User API](./sdk-api-user.md) | API key (`X-Auth-Token`) | Read user profiles, manage personal access tokens. | ## Python credential helpers The `qhub.api.credentials` module exposes `CredentialProvider` implementations you can compose to resolve a token for any of the four clients. Every helper returns the access token string; wire it into the client by passing `api_key=` (Quantum, Platform, User) or `token=` (Service). | Provider | Where it looks | | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `StaticCredential(token)` | The value you pass in. | | `EnvironmentCredential()` | `KQH_SERVICE_EXECUTION_TOKEN`, then `KQH_PERSONAL_ACCESS_TOKEN`, then legacy `PLANQK_SERVICE_EXECUTION_TOKEN`, `SERVICE_EXECUTION_TOKEN`, `PLANQK_PERSONAL_ACCESS_TOKEN`. | | `ConfigFileCredential()` | JSON file at `KQH_CONFIG_FILE_PATH` env var, else `PLANQK_CONFIG_FILE_PATH`, else `~/.config/qhubctl/config.json` (`%LOCALAPPDATA%\qhubctl\config.json` on Windows), falling back to the legacy planqk path. Expects `{"auth": {"value": ""}}`. | | `DefaultCredentialsProvider(access_token=None)` | Tries `StaticCredential`, then `EnvironmentCredential`, then `ConfigFileCredential`, in that order. | If no credential can be resolved, every helper raises `CredentialUnavailableError`. ::: tabs key:pythonTS \== Python ```python from qhub.api.credentials import DefaultCredentialsProvider from qhub.api.quantum import HubQuantumClient token = DefaultCredentialsProvider().get_access_token() client = HubQuantumClient(api_key=token) ``` \== TypeScript ```ts import { HubQuantumClient } from "@quantum-hub/qhub-api/quantum"; const client = new HubQuantumClient({ apiKey: process.env.KQH_PERSONAL_ACCESS_TOKEN!, }); ``` ::: ## Environment variables | Variable | Purpose | | --------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | `KQH_SERVICE_EXECUTION_TOKEN` | Service execution token; preferred token env var. | | `KQH_PERSONAL_ACCESS_TOKEN` | Personal access token. | | `KQH_CONFIG_FILE_PATH` | Override the path to the shared config file. | | `KQH_ORGANIZATION_ID` | Default organization id for `ContextResolver`. | | `PLANQK_SERVICE_EXECUTION_TOKEN` / `SERVICE_EXECUTION_TOKEN` / `PLANQK_PERSONAL_ACCESS_TOKEN` | Legacy token env vars, still honoured. | | `PLANQK_CONFIG_FILE_PATH` | Legacy config-file env var, still honoured. | | `PLANQK_ORGANIZATION_ID` | Legacy organization-id env var, still honoured. | None of these are auto-consumed by client constructors - use `DefaultCredentialsProvider` (Python) or read `process.env` yourself (TypeScript). --- --- url: /sdk-api-quantum.md description: >- Submit quantum jobs and manage sessions, list and inspect backends, and stream results through the HubQuantumClient. --- # HubQuantumClient > Part of the [Kipu Quantum Hub API SDK reference](./sdk-api.md) - see the landing page for [installation](./sdk-api.md#installation) and [Python credential helpers](./sdk-api.md#python-credential-helpers). Use `HubQuantumClient` to run jobs and manage sessions. It is the most common entry point for users of the SDK. ## Authentication `HubQuantumClient` authenticates with a personal access token or a service execution token, sent as the `X-Auth-Token` request header. Tokens are never cached or refreshed by the SDK itself - the caller provides a valid credential and rotates service execution tokens before they expire. Python ships optional credential helpers in `qhub.api.credentials` that read the token from the environment or the shared `qhubctl` config file (see [Python credential helpers](./sdk-api.md#python-credential-helpers)). They are not wired into the client constructors automatically; pass the resolved token in via `api_key` yourself. The TypeScript SDK does not ship credential helpers; read `process.env.KQH_PERSONAL_ACCESS_TOKEN` (or your own variable) and pass it via `apiKey`. ## Organization scoping Most Quantum endpoints accept an `X-OrganizationId` header to scope the request to a specific organization. `HubQuantumClient` exposes a dedicated `organization_id` / `organizationId` constructor option that sends the header on every request; in TypeScript, individual sub-client methods also accept a per-request override. Construct one client per organization if you need to switch at runtime. The Python `qhub.api.context.ContextResolver` reads the `qhubctl` config file and honours the `KQH_ORGANIZATION_ID` (and legacy `PLANQK_ORGANIZATION_ID`) env vars, but, like credentials, you must feed the resolved id into the client yourself. ## Quickstart Submit a job, wait for it, and fetch the result. ::: tabs key:pythonTS \== Python ```python import time from qhub.api.quantum import HubQuantumClient from qhub.api.quantum.jobs import CreateJobRequestInput_AzureIonqSimulator from qhub.api.quantum.types import AzureIonqJobInputCircuitItem client = HubQuantumClient(api_key="YOUR_PERSONAL_ACCESS_TOKEN") job = client.jobs.create_job( backend_id="azure.ionq.simulator", shots=1000, input=CreateJobRequestInput_AzureIonqSimulator( circuit=[ AzureIonqJobInputCircuitItem(targets=[0]), AzureIonqJobInputCircuitItem(targets=[1], controls=[0]), ], gateset="qis", qubits=2, ), ) while client.jobs.get_job_status(job.id).status in ("PENDING", "RUNNING"): time.sleep(2) print(client.jobs.get_job_result(job.id)) ``` \== TypeScript ```ts import { HubQuantumClient } from "@quantum-hub/qhub-api/quantum"; const client = new HubQuantumClient({ apiKey: process.env.KQH_PERSONAL_ACCESS_TOKEN!, }); const job = await client.jobs.createJob({ backend_id: "aws.sim.sv1", shots: 1000, input: { type: "AZURE_IONQ_SIMULATOR", circuit: [{ targets: [0] }, { targets: [1], controls: [0] }], gateset: "qis", qubits: 2, }, }); while (true) { const status = await client.jobs.getJobStatus(job.id!); if (status.status !== "PENDING" && status.status !== "RUNNING") break; await new Promise((r) => setTimeout(r, 2000)); } console.log(await client.jobs.getJobResult(job.id!)); ``` ::: ## Constructor | Parameter | Required | Description | | ------------------------------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------- | | `api_key` / `apiKey` | yes | Personal access token or service execution token; sent as `X-Auth-Token`. | | `organization_id` / `organizationId` | no | Value for the `X-OrganizationId` header; sent on every request from this client. | | `base_url` / `baseUrl` | no | Overrides both the default environment and `environment` if supplied. | | `environment` | no | `HubQuantumClientEnvironment.DEFAULT` (Python) / `HubQuantumEnvironment.Default` (TypeScript), see [Endpoints](#endpoints). | | `headers` | no | Additional headers merged into every request. | | `timeout` / `timeoutInSeconds` | no | Read timeout in seconds; defaults to 60 when no custom HTTP client is supplied. | | `max_retries` / `maxRetries` | no | Number of retries for transient failures; defaults to 2. | | `follow_redirects` | no (Python only) | Passed through to `httpx.Client`; defaults to `True`. | | `httpx_client` / `fetch` | no | Inject a preconfigured HTTP client (Python) or `fetch` implementation (TypeScript). | | `logging` | no | Logger instance or `{level, logger, silent}` config dict. | Python also ships `AsyncHubQuantumClient` with the same shape plus an `httpx.AsyncClient` hook. ::: tabs key:pythonTS \== Python ```python from qhub.api.quantum import HubQuantumClient client = HubQuantumClient( api_key="YOUR_PERSONAL_ACCESS_TOKEN", organization_id="YOUR_ORGANIZATION_ID", timeout=120, max_retries=3, ) ``` \== TypeScript ```ts import { HubQuantumClient } from "@quantum-hub/qhub-api/quantum"; const client = new HubQuantumClient({ apiKey: process.env.KQH_PERSONAL_ACCESS_TOKEN!, organizationId: process.env.KQH_ORGANIZATION_ID, timeoutInSeconds: 120, maxRetries: 3, }); ``` ::: ## Behaviour * `base_url` / `baseUrl` always wins over `environment`. * `timeout` is ignored in Python when a preconfigured `httpx_client` is supplied; configure the timeout on the injected client instead. * `max_retries` applies to transient failures (network errors and 5xx responses); per-request overrides in `request_options` (Python) or per-call options (TypeScript) take precedence. ## Namespaces `HubQuantumClient` exposes four sub-clients, lazily instantiated on first access. | Namespace | Purpose | | ----------- | --------------------------------------------------------------- | | `backends` | Discover backends and read their configuration and calibration. | | `sessions` | Open, inspect, and close quantum sessions. | | `jobs` | Submit, monitor, retrieve, and cancel jobs (session or not). | | `workloads` | List jobs and sessions as a single paginated stream. | ## Backends Backends describe the hardware or simulators you can target. | Method | Description | | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `backends.get_backends(provider=?, only_planqk_sdk=?)` / `backends.getBackends({ provider?, onlyPlanqkSdk? })` | List backends, optionally filtered by provider. | | `backends.get_backend(id)` / `backends.getBackend(id)` | Fetch one backend with full configuration metadata. | | `backends.get_backend_status(id)` / `backends.getBackendStatus(id)` | Current status (`ONLINE`, `PAUSED`, `OFFLINE`, `RETIRED`). | | `backends.get_backend_config(id)` / `backends.getBackendConfig(id)` | Raw backend configuration as a JSON object. | | `backends.get_backend_calibration(id, effective_at=?)` / `backends.getBackendCalibration(id, { effectiveAt? })` | Calibration, optionally at a historical timestamp. | | `backends.get_least_busy_backend(provider, min_qubits=?)` / `backends.getLeastBusyBackend({ provider, minQubits? })` | Backend with the lowest queue size for a provider (IBM-only today). | ### get\_backends List backends, optionally filtered by provider. Publicly accessible: authenticated callers receive full operational details, unauthenticated callers receive a reduced view. | Parameter | Required | Type | Default | Description | | ----------------------------------- | -------- | --------------------- | ------- | -------------------------------------------------------------------------------------------------------- | | `provider` | no | `str?` / `string?` | all | Filter by `AZURE`, `AWS`, `IBM`, `QRYD`, `QUDORA`, `QUANDELA`, `IQM` (case-insensitive; hyphens become underscores). | | `only_planqk_sdk` / `onlyPlanqkSdk` | no | `bool?` / `boolean?` | `false` | When `true`, return only backends usable via the qhub-quantum SDK. | ::: tabs key:pythonTS \== Python ```python for backend in client.backends.get_backends(provider="IBM", only_planqk_sdk=True): print(backend.id, backend.queue_size) ``` \== TypeScript ```ts const backends = await client.backends.getBackends({ provider: "IBM", onlyPlanqkSdk: true, }); for (const backend of backends) { console.log(backend.id, backend.queue_size); } ``` ::: Returns `List[Backend]` / `Backend[]` — see [Backend](#backend). ### get\_backend Fetch one backend by id with full configuration metadata. | Parameter | Required | Type | Default | Description | | --------- | -------- | ------------------ | ------- | ---------------------------------------- | | `id` | yes | `str` / `string` | — | Backend identifier (e.g. `aws.sim.sv1`). | ::: tabs key:pythonTS \== Python ```python backend = client.backends.get_backend("aws.sim.sv1") ``` \== TypeScript ```ts const backend = await client.backends.getBackend("aws.sim.sv1"); ``` ::: Returns `Backend` — see [Backend](#backend). ### get\_backend\_status Lightweight operational state, separate from the rich `Backend` record so callers can poll cheaply. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | -------------------- | | `id` | yes | `str` / `string` | — | Backend identifier. | ::: tabs key:pythonTS \== Python ```python state = client.backends.get_backend_status("aws.sim.sv1") print(state.status, state.queue_size, state.queue_avg_time) ``` \== TypeScript ```ts const state = await client.backends.getBackendStatus("aws.sim.sv1"); console.log(state.status, state.queue_size, state.queue_avg_time); ``` ::: Returns `BackendStateInfo`: | Field | Type | Description | | ---------------- | -------------------------- | ----------------------------------------------------------------- | | `status` | `BackendStateInfoStatus?` | `ONLINE`, `PAUSED`, `OFFLINE`, `RETIRED`, or `UNKNOWN`. | | `queue_size` | `int?` | Current queue length on the provider side. | | `queue_avg_time` | `int?` | Average queue waiting time in seconds, as reported by the provider. | ### get\_backend\_config Raw, provider-specific configuration JSON for the backend (gate set, qubit count, coupling map, and so on). | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | ------------------- | | `id` | yes | `str` / `string` | — | Backend identifier. | ::: tabs key:pythonTS \== Python ```python config = client.backends.get_backend_config("aws.sim.sv1") ``` \== TypeScript ```ts const config = await client.backends.getBackendConfig("aws.sim.sv1"); ``` ::: Returns a free-form JSON object (`Dict[str, Any]` / `Record`). ### get\_backend\_calibration Calibration snapshot at the latest known instant, or at a historical instant if `effective_at` is supplied. Returns 204 when no calibration exists at or before the requested timestamp. | Parameter | Required | Type | Default | Description | | ------------------------------- | -------- | -------------------------- | ------- | ----------------------------------------------------------------- | | `id` | yes | `str` / `string` | — | Backend identifier. | | `effective_at` / `effectiveAt` | no | `datetime?` / `Date?` | latest | ISO-8601 UTC timestamp; returns the calibration effective at that instant. | ::: tabs key:pythonTS \== Python ```python import datetime as dt cal = client.backends.get_backend_calibration( "aws.sim.sv1", effective_at=dt.datetime.fromisoformat("2026-04-17T12:34:56+00:00"), ) ``` \== TypeScript ```ts const cal = await client.backends.getBackendCalibration("aws.sim.sv1", { effectiveAt: new Date("2026-04-17T12:34:56Z"), }); ``` ::: Returns `CalibrationResponse?`: | Field | Type | Description | | --------------- | ----------------------------------------------- | ------------------------------------------------------------------------ | | `backend_id` | `str?` | Backend the snapshot belongs to. | | `calibrated_at` | `datetime?` (ISO 8601) | Instant the calibration was effective on the backend. | | `calibration` | `Dict[str, Any]?` / `Record?` | Provider-shaped calibration payload; shape depends on backend technology. | ### get\_least\_busy\_backend Backend with the lowest reported queue size for a provider. **IBM-only today** — other providers return 400; 404 if no eligible backend is found. | Parameter | Required | Type | Default | Description | | --------------------------- | -------- | ------------------- | --------- | -------------------------------------------------------- | | `provider` | yes | `str` / `string` | — | Quantum provider (IBM only at present). | | `min_qubits` / `minQubits` | no | `int?` / `number?` | unbounded | Exclude backends with fewer qubits than this. | ::: tabs key:pythonTS \== Python ```python ibm = client.backends.get_least_busy_backend(provider="IBM", min_qubits=5) print(ibm.id, ibm.queue_size) ``` \== TypeScript ```ts const ibm = await client.backends.getLeastBusyBackend({ provider: "IBM", minQubits: 5, }); console.log(ibm.id, ibm.queue_size); ``` ::: Returns `Backend` — see [Backend](#backend). ## Sessions Sessions group jobs on the same backend to amortize provider startup cost. ``` OPEN ──► ACTIVE ──► DRAINING ──► CLOSED │ │ │ └──► INACTIVE └──────────────► ABORTED ``` `UNKNOWN` is used for any state the server has not yet resolved (for example, right after creation). | Method | Description | | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `sessions.create_session(backend_id, mode, provider, ttl=?, tags=?, metadata=?, sdk_provider=?)` | Open a new session. `mode` is `batch` or `dedicated`; `ttl` is max lifetime in seconds. | | `sessions.get_session(id)` | Full session record. | | `sessions.get_session_status(id)` | Terse `{status}` view for polling. | | `sessions.get_session_jobs(id)` | Jobs attached to this session. | | `sessions.update_session_state(id, accept_jobs=bool)` | Stop or resume accepting new jobs. | | `sessions.close_session(id)` | Close the session; subsequent submissions are rejected. | TypeScript exposes the same methods in camelCase (`sessions.createSession({ backend_id, mode, provider, ttl?, tags?, metadata?, sdk_provider? })`, `sessions.getSession(id)`, `sessions.getSessionStatus(id)`, `sessions.getSessionJobs(id)`, `sessions.updateSessionState(id, { accept_jobs })`, `sessions.closeSession(id)`). ### create\_session Open a new session that groups related jobs on a shared backend. Returns 422 if the selected backend does not support sessions. | Parameter | Required | Type | Default | Description | | -------------- | -------- | ----------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------ | | `backend_id` | yes | `str` / `string` | — | Backend identifier. | | `mode` | yes | `SessionMode` | — | `batch` (queued jobs share priority) or `dedicated` (reserves the backend). | | `provider` | yes | `SessionProvider` | — | Cloud provider exposing the backend. | | `ttl` | no | `int?` / `number?` | provider default | Max session lifetime in seconds. | | `tags` | no | `List[str]?` / `string[]?` | — | Free-form labels for categorization or filtering. | | `metadata` | no | `Dict[str, Any]?` / `Record?` | — | Free-form metadata stored on the session. | | `sdk_provider` | no | `SessionSdkProvider?` | — | `QISKIT`, `BRAKET`, `PERCEVAL`, or `CLIENT`. | ::: tabs key:pythonTS \== Python ```python session = client.sessions.create_session( backend_id="aws.sim.sv1", mode="batch", provider="AWS", ttl=900, tags=["demo"], ) ``` \== TypeScript ```ts const session = await client.sessions.createSession({ backend_id: "aws.sim.sv1", mode: "batch", provider: "AWS", ttl: 900, tags: ["demo"], }); ``` ::: Returns `Session` — see [Session](#session). Newly-created sessions start non-final and transition through the lifecycle in [Sessions](#sessions). ### get\_session Fetch a session's full record. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | ------------------- | | `id` | yes | `str` / `string` | — | Session identifier. | ::: tabs key:pythonTS \== Python ```python session = client.sessions.get_session(session_id) ``` \== TypeScript ```ts const session = await client.sessions.getSession(sessionId); ``` ::: Returns `Session`. ### get\_session\_status Terse status-only view, cheaper to poll than `get_session`. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | ------------------- | | `id` | yes | `str` / `string` | — | Session identifier. | ::: tabs key:pythonTS \== Python ```python status = client.sessions.get_session_status(session_id).status ``` \== TypeScript ```ts const { status } = await client.sessions.getSessionStatus(sessionId); ``` ::: Returns `SessionStatusResponse` with one field `status: SessionStatusResponseStatus?` — see [Sessions](#sessions) for the lifecycle. ### get\_session\_jobs List all jobs that have been submitted to this session. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | ------------------- | | `id` | yes | `str` / `string` | — | Session identifier. | ::: tabs key:pythonTS \== Python ```python jobs = client.sessions.get_session_jobs(session_id) ``` \== TypeScript ```ts const jobs = await client.sessions.getSessionJobs(sessionId); ``` ::: Returns a list of `Job` records — see [Job](#job). ### update\_session\_state Toggle whether the session accepts new job submissions. Set `accept_jobs=false` to drain (finish in-flight jobs, reject new ones) without closing; to terminate, call `close_session` instead. | Parameter | Required | Type | Default | Description | | ------------- | -------- | ------------------- | ------- | --------------------------------------------- | | `id` | yes | `str` / `string` | — | Session identifier. | | `accept_jobs` | yes | `bool` / `boolean` | — | `true` → `ACTIVE`, `false` → `DRAINING`. | ::: tabs key:pythonTS \== Python ```python client.sessions.update_session_state(session_id, accept_jobs=False) ``` \== TypeScript ```ts await client.sessions.updateSessionState(sessionId, { accept_jobs: false }); ``` ::: Returns the updated `Session`. ### close\_session Close the session and release reserved backend resources. Already-submitted jobs are left to finish; no new jobs can be submitted once the session is closed. Idempotent — closing an already-closed session has no effect. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | ------------------- | | `id` | yes | `str` / `string` | — | Session identifier. | ::: tabs key:pythonTS \== Python ```python client.sessions.close_session(session_id) ``` \== TypeScript ```ts await client.sessions.closeSession(sessionId); ``` ::: No body returned. ### Example - batch session on AWS Braket ::: tabs key:pythonTS \== Python ```python session = client.sessions.create_session( backend_id="aws.sim.sv1", mode="batch", provider="AWS", ttl=900, ) try: for i in range(5): client.jobs.create_job( backend_id="aws.sim.sv1", shots=500, input=my_input, session_id=session.id, tags=[f"iter-{i}"], ) finally: client.sessions.close_session(session.id) ``` \== TypeScript ```ts const session = await client.sessions.createSession({ backend_id: "aws.sim.sv1", mode: "batch", provider: "AWS", ttl: 900, }); try { for (let i = 0; i < 5; i++) { await client.jobs.createJob({ backend_id: "aws.sim.sv1", shots: 500, input: myInput, session_id: session.id, tags: [`iter-${i}`], }); } } finally { await client.sessions.closeSession(session.id!); } ``` ::: ## Jobs ``` PENDING ──► RUNNING ──► COMPLETED │ │ │ ├──► FAILED │ ├──► CANCELLING ──► CANCELLED │ └──► ABORTED └──► CANCELLING ──► CANCELLED ``` `UNKNOWN` covers states the server has not yet resolved. Terminal states are `COMPLETED`, `FAILED`, `ABORTED`, and `CANCELLED`. | Method | Description | | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `jobs.search_jobs(page=?, size=?, sort=?, service_execution_id=?)` | Paginated listing, optionally filtered by service execution. | | `jobs.create_job(backend_id, shots, input, name=?, input_format=?, input_params=?, tags=?, session_id=?, sdk_provider=?)` | Submit a new job. | | `jobs.get_job(id)` | Full job record. | | `jobs.get_job_status(id)` | Terse `{status}` view for polling. | | `jobs.get_job_result(id)` | Parsed result body (JSON). | | `jobs.get_job_result_stream(id)` | Stream the result file in chunks (Python iterator / Node stream). | | `jobs.get_job_input(id)` | Input file (string). | | `jobs.get_job_calibration(id)` | Calibration snapshot captured at execution time. | | `jobs.cancel_job(id)` | Request cancellation; state transitions to `CANCELLING`. | TypeScript exposes the same methods in camelCase (`jobs.searchJobs({ page?, size?, sort?, serviceExecutionId? })`, `jobs.createJob({ ... })`, `jobs.getJob(id)`, and so on). A second family of methods with the `service_execution_` prefix (Python) / `ServiceExecution` suffix (TypeScript) addresses jobs and sessions that belong to a managed service execution - for example, `jobs.get_service_execution_job(service_execution_id, job_id)` / `jobs.getServiceExecutionJob(serviceExecutionId, jobId)`, `jobs.close_service_execution_session(...)` / `jobs.closeServiceExecutionSession(...)`, `jobs.cancel_service_execution_job(...)` / `jobs.cancelServiceExecutionJob(...)`, and so on. Use the plain methods when you created the job yourself; use the service-execution-scoped methods when the job was created by a managed service on your behalf. ### search\_jobs Paginated job listing. | Parameter | Required | Type | Default | Description | | ----------------------------------------------- | -------- | -------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------- | | `page` | no | `int?` / `number?` | `0` | Zero-based page index. | | `size` | no | `int?` / `number?` | server default | Page size. | | `sort` | no | `str \| List[str]?` / `string \| string[]?` | — | `field,asc\|desc` Spring-style; pass a sequence for multi-field sort. | | `service_execution_id` / `serviceExecutionId` | no | `str?` / `string?` | — | Restrict to jobs created by a managed service execution. | ::: tabs key:pythonTS \== Python ```python page = client.jobs.search_jobs(page=0, size=20, sort="created_at,desc") for job in page.content or []: print(job.id, job.status) ``` \== TypeScript ```ts const page = await client.jobs.searchJobs({ page: 0, size: 20, sort: "created_at,desc", }); for (const job of page.content ?? []) { console.log(job.id, job.status); } ``` ::: Returns `PageResponseJob` — see [Pagination wrappers](#pagination-wrappers). ### create\_job Submit a new job for asynchronous execution. The returned job starts non-terminal; poll `get_job_status` until it reaches a terminal state, then retrieve the result with `get_job_result`. | Parameter | Required | Type | Default | Description | | -------------- | -------- | --------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- | | `backend_id` | yes | `str` / `string` | — | Target backend identifier. | | `shots` | yes | `int` / `number` | — | Number of shots to execute. | | `input` | yes | `CreateJobRequestInput` | — | Backend-specific job input (e.g. `AzureIonqJobInput`, `IbmJobInput`, `AwsAhsJobInput`, `AwsQasm3JobInput`, `IqmJobInput`, `KipuJobInput`, `QudoraJobInput`, `QuandelaJobInput`). | | `name` | no | `str?` / `string?` | — | Human-readable job name. | | `input_format` | no | `JobInputFormat?` | inferred | Format of the submitted input; must match the backend's `supportedInputFormats`. | | `input_params` | no | `CreateJobRequestInputParams?` | — | Backend-specific parameters. | | `tags` | no | `List[str]?` / `string[]?` | — | Free-form labels. | | `session_id` | no | `str?` / `string?` | — | Attach the job to an open session. | | `sdk_provider` | no | `JobSdkProvider?` | — | `QISKIT`, `BRAKET`, `PERCEVAL`, or `CLIENT`. | ::: tabs key:pythonTS \== Python ```python from qhub.api.quantum.jobs import CreateJobRequestInput_AzureIonqSimulator from qhub.api.quantum.types import AzureIonqJobInputCircuitItem job = client.jobs.create_job( backend_id="kipu.sim.qsim", shots=1000, input=CreateJobRequestInput_AzureIonqSimulator( circuit=[ AzureIonqJobInputCircuitItem(targets=[0]), AzureIonqJobInputCircuitItem(targets=[1], controls=[0]), ], gateset="qis", qubits=2, ), name="bell-pair", tags=["demo"], ) ``` \== TypeScript ```ts const job = await client.jobs.createJob({ backend_id: "kipu.sim.qsim", shots: 1000, input: { type: "AZURE_IONQ_SIMULATOR", circuit: [{ targets: [0] }, { targets: [1], controls: [0] }], gateset: "qis", qubits: 2, }, name: "bell-pair", tags: ["demo"], }); ``` ::: Returns `Job` — see [Job](#job). ### get\_job Full job record, including timestamps, backend, and session affiliation. Use `get_job_status` when only the lifecycle status is needed. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | Job identifier. | ::: tabs key:pythonTS \== Python ```python job = client.jobs.get_job(job_id) ``` \== TypeScript ```ts const job = await client.jobs.getJob(jobId); ``` ::: Returns `Job`. ### get\_job\_status Cheap status-only poll. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | Job identifier. | ::: tabs key:pythonTS \== Python ```python status = client.jobs.get_job_status(job_id).status ``` \== TypeScript ```ts const { status } = await client.jobs.getJobStatus(jobId); ``` ::: Returns `JobStatusResponse` with one field `status: JobStatusResponseStatus?` — see [Jobs](#jobs) for the lifecycle. ### get\_job\_result Measurement results as inline JSON. Only valid once the job is in a terminal result state (`COMPLETED` or `FAILED`); calling it earlier returns 404. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | Job identifier. | ::: tabs key:pythonTS \== Python ```python result = client.jobs.get_job_result(job_id) ``` \== TypeScript ```ts const result = await client.jobs.getJobResult(jobId); ``` ::: Returns a free-form JSON object (`Dict[str, Any]` / `Record`); shape depends on the backend. ### get\_job\_result\_stream Same data as `get_job_result`, streamed as bytes — use for large result files. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | Job identifier. | Returns `Iterator[bytes]` (Python) / `ReadableStream` (TypeScript). See [Streaming the result file](#streaming-the-result-file) for a runnable example. ### get\_job\_input The exact input file submitted via `create_job`, returned as a JSON object. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | Job identifier. | ::: tabs key:pythonTS \== Python ```python inp = client.jobs.get_job_input(job_id) ``` \== TypeScript ```ts const inp = await client.jobs.getJobInput(jobId); ``` ::: Returns `Dict[str, Any]` / `Record`. ### get\_job\_calibration Calibration snapshot captured at execution time. Returns `None` / `undefined` when no calibration is available for the job's backend. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | Job identifier. | ::: tabs key:pythonTS \== Python ```python cal = client.jobs.get_job_calibration(job_id) ``` \== TypeScript ```ts const cal = await client.jobs.getJobCalibration(jobId); ``` ::: Returns `CalibrationResponse?` — same shape as [get\_backend\_calibration](#get_backend_calibration). ### cancel\_job Request cancellation; state transitions to `CANCELLING` and then `CANCELLED`. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | Job identifier. | ::: tabs key:pythonTS \== Python ```python client.jobs.cancel_job(job_id) ``` \== TypeScript ```ts await client.jobs.cancelJob(jobId); ``` ::: No body returned. ### Service-execution-scoped methods These mirror the plain job and session methods but address workloads owned by a managed service execution. Use them when the job or session was created by a managed service on your behalf. | Method | Description | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `jobs.get_service_execution_workloads(service_execution_id, page=?, size=?, sort=?)` / `jobs.getServiceExecutionWorkloads(serviceExecutionId, { page?, size?, sort? })` | Paginated workloads (jobs + sessions) running inside the execution. | | `jobs.get_service_execution_session_status(service_execution_id, session_id)` / `jobs.getServiceExecutionSessionStatus(serviceExecutionId, sessionId)` | Status of a session inside an execution. | | `jobs.get_service_execution_session_jobs(service_execution_id, session_id)` / `jobs.getServiceExecutionSessionJobs(serviceExecutionId, sessionId)` | Jobs belonging to a session inside an execution. | | `jobs.update_service_execution_session_state(service_execution_id, session_id, accept_jobs=bool)` / `jobs.updateServiceExecutionSessionState(serviceExecutionId, sessionId, { accept_jobs })` | Drain or resume a session inside an execution. | | `jobs.close_service_execution_session(service_execution_id, session_id)` / `jobs.closeServiceExecutionSession(serviceExecutionId, sessionId)` | Close a session inside an execution. | | `jobs.get_service_execution_job(service_execution_id, job_id)` / `jobs.getServiceExecutionJob(serviceExecutionId, jobId)` | Fetch one job inside an execution. | | `jobs.get_service_execution_job_status(service_execution_id, job_id)` / `jobs.getServiceExecutionJobStatus(serviceExecutionId, jobId)` | Status-only view for the same job. | | `jobs.get_service_execution_job_input(service_execution_id, job_id)` / `jobs.getServiceExecutionJobInput(serviceExecutionId, jobId)` | Input JSON for the job. | | `jobs.get_service_execution_job_result_stream(service_execution_id, job_id)` / `jobs.getServiceExecutionJobResultStream(serviceExecutionId, jobId)` | Stream the job's result file. | | `jobs.get_service_execution_job_calibration(service_execution_id, job_id)` / `jobs.getServiceExecutionJobCalibration(serviceExecutionId, jobId)` | Calibration snapshot for the job. | | `jobs.cancel_service_execution_job(service_execution_id, job_id)` / `jobs.cancelServiceExecutionJob(serviceExecutionId, jobId)` | Cancel the job. | ::: tabs key:pythonTS \== Python ```python workloads = client.jobs.get_service_execution_workloads( service_execution_id=execution_id, page=0, size=50, sort="created_at,desc", ) for w in workloads.content or []: print(w.id, w.type, w.status) job = client.jobs.get_service_execution_job(execution_id, job_id) ``` \== TypeScript ```ts const workloads = await client.jobs.getServiceExecutionWorkloads(executionId, { page: 0, size: 50, sort: "created_at,desc", }); for (const w of workloads.content ?? []) { console.log(w.id, w.type, w.status); } const job = await client.jobs.getServiceExecutionJob(executionId, jobId); ``` ::: Return shapes mirror the plain methods: `PageResponseWorkloadResponse` for the workload listing, `Job` for single-job lookups, `SessionStatusResponse` and `JobStatusResponse` for the status-only variants, `Iterator[bytes]` / `ReadableStream` for the result stream, `Dict[str, Any]` / `Record` for the input file, `CalibrationResponse?` for calibration, and no body for cancel and close. ### Polling for completion ::: tabs key:pythonTS \== Python ```python import time TERMINAL = {"COMPLETED", "FAILED", "ABORTED", "CANCELLED"} def wait(job_id: str, poll_interval: float = 2.0) -> str: while True: status = client.jobs.get_job_status(job_id).status if status in TERMINAL: return status time.sleep(poll_interval) final = wait(job.id) if final == "COMPLETED": result = client.jobs.get_job_result(job.id) ``` \== TypeScript ```ts const TERMINAL = new Set(["COMPLETED", "FAILED", "ABORTED", "CANCELLED"]); async function wait(jobId: string, pollMs = 2000): Promise { for (;;) { const { status } = await client.jobs.getJobStatus(jobId); if (status && TERMINAL.has(status)) return status; await new Promise((r) => setTimeout(r, pollMs)); } } const final = await wait(job.id!); if (final === "COMPLETED") { const result = await client.jobs.getJobResult(job.id!); } ``` ::: ### Streaming the result file Large results are better consumed as a byte stream; the `*_stream` / `*Stream` variants hand you chunks rather than parsing the full JSON into memory. ::: tabs key:pythonTS \== Python ```python with open("result.json", "wb") as out: for chunk in client.jobs.get_job_result_stream(job.id): out.write(chunk) ``` \== TypeScript ```ts import { Writable } from "node:stream"; import { createWriteStream } from "node:fs"; const stream = await client.jobs.getJobResultStream(job.id!); await stream.pipeTo(Writable.toWeb(createWriteStream("result.json"))); ``` ::: ## Workloads `workloads.get_workloads(...)` / `workloads.getWorkloads({ ... })` returns a single paginated list mixing jobs and sessions, tagged with a `type` discriminator (`JOB` or `SESSION`). Use it when you want a uniform "show me everything running" view; otherwise reach for `jobs.search_jobs` or list sessions individually. ### get\_workloads | Parameter | Required | Type | Default | Description | | --------- | -------- | -------------------------------------------- | -------------- | ---------------------------------------------------------------------- | | `page` | no | `int?` / `number?` | `0` | Zero-based page index. | | `size` | no | `int?` / `number?` | server default | Page size. | | `sort` | no | `str \| List[str]?` / `string \| string[]?` | — | `field,asc\|desc` Spring-style; pass a sequence for multi-field sort. | ::: tabs key:pythonTS \== Python ```python page = client.workloads.get_workloads(page=0, size=50, sort="created_at,desc") for w in page.content or []: print(w.id, w.type, w.backend_id, w.status) ``` \== TypeScript ```ts const page = await client.workloads.getWorkloads({ page: 0, size: 50, sort: "created_at,desc", }); for (const w of page.content ?? []) { console.log(w.id, w.type, w.backend_id, w.status); } ``` ::: Returns `PageResponseWorkloadResponse`. Each `WorkloadResponse` has: | Field | Type | Description | | ------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------- | | `id` | `str?` | Resolves to a job id or session id depending on `type`. | | `type` | `WorkloadResponseType?` | `JOB` or `SESSION`. | | `backend_id` | `str?` | Backend the workload targets. | | `provider` | `WorkloadResponseProvider?` | Cloud provider exposing the backend. | | `status` | `str?` | `JobStatus` value when `type=JOB`, `SessionStatus` value when `type=SESSION`. | | `created_at` / `started_at` / `ended_at` | `str?` (ISO 8601) | Lifecycle timestamps. | | `sdk_provider` | `WorkloadResponseSdkProvider?` | SDK that produced the workload. | ## Advanced usage ### Running N jobs in parallel ::: tabs key:pythonTS \== Python ```python import asyncio from qhub.api.quantum import AsyncHubQuantumClient async def run_batch(inputs): client = AsyncHubQuantumClient(api_key="YOUR_TOKEN") jobs = await asyncio.gather(*( client.jobs.create_job(backend_id="aws.sim.sv1", shots=1000, input=i) for i in inputs )) return [j.id for j in jobs] ``` \== TypeScript ```ts import { HubQuantumClient } from "@quantum-hub/qhub-api/quantum"; async function runBatch(client: HubQuantumClient, inputs: unknown[]) { const jobs = await Promise.all( inputs.map((input) => client.jobs.createJob({ backend_id: "aws.sim.sv1", shots: 1000, input }), ), ); return jobs.map((j) => j.id!); } ``` ::: ### Polling with exponential backoff ::: tabs key:pythonTS \== Python ```python import random, time def wait_with_backoff(job_id, initial=1.0, cap=30.0): delay = initial while True: status = client.jobs.get_job_status(job_id).status if status in ("COMPLETED", "FAILED", "ABORTED", "CANCELLED"): return status time.sleep(delay + random.uniform(0, delay * 0.1)) delay = min(delay * 2, cap) ``` \== TypeScript ```ts async function waitWithBackoff(jobId: string, initialMs = 1000, capMs = 30000) { let delay = initialMs; for (;;) { const { status } = await client.jobs.getJobStatus(jobId); if ( status && ["COMPLETED", "FAILED", "ABORTED", "CANCELLED"].includes(status) ) { return status; } await new Promise((r) => setTimeout(r, delay + Math.random() * delay * 0.1), ); delay = Math.min(delay * 2, capMs); } } ``` ::: ### Fetch passthrough (TypeScript) The TypeScript client exposes `client.fetch(input, init?, requestOptions?)`, which reuses the client's base URL, auth provider, retry policy, and logging to call an endpoint that does not yet have a typed wrapper. Python does not ship a public equivalent; for that case, use `client._client_wrapper` at your own risk or drop down to `httpx` directly. ## Endpoints | Default base URL | Override | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | `https://api.hub.kipu-quantum.com/quantum` | `base_url=...` / `baseUrl: ...`, or `environment=HubQuantumClientEnvironment.DEFAULT` (Python) / `environment: HubQuantumEnvironment.Default` (TypeScript). | To target a staging or on-prem deployment, set `base_url` / `baseUrl` at construction time. ## Errors HTTP errors are raised as typed exceptions. All Python errors extend `ApiError` (in `qhub.api.quantum.core.api_error`); all TypeScript errors extend `HubQuantumError`. | Status | Python class | TypeScript class | Meaning | | ------ | --------------------------- | --------------------------- | --------------------------------------- | | 400 | `BadRequestError` | `BadRequestError` | Malformed request. | | 401 | `UnauthorizedError` | `UnauthorizedError` | Missing or invalid credentials. | | 403 | `ForbiddenError` | `ForbiddenError` | Authenticated but not permitted. | | 404 | `NotFoundError` | `NotFoundError` | Resource does not exist. | | 422 | `UnprocessableEntityError` | `UnprocessableEntityError` | Validation failure on the request body. | | 500 | `InternalServerError` | `InternalServerError` | Server-side failure. | Other failure modes: * **Auth misconfiguration (TypeScript)** - constructing `HubQuantumClient` without `apiKey` throws `HubQuantumError` with message `"Please provide 'apiKey' when initializing the client"`. * **Timeouts** - hitting `timeoutInSeconds` raises `HubQuantumTimeoutError` in TypeScript; in Python, timeouts surface as the underlying `httpx.TimeoutException`. * **Missing base URL** - passing `environment=None` *and* omitting `base_url` to the Python constructor raises `Exception("Please pass in either base_url or environment to construct the client")`. All HTTP error instances expose `status_code`, `headers`, and `body` (Python) / `statusCode`, `rawResponse`, and `body` (TypeScript) so you can inspect the server's response detail. ::: tabs key:pythonTS \== Python ```python from qhub.api.quantum.errors import NotFoundError, UnauthorizedError try: client.jobs.get_job("does-not-exist") except NotFoundError as e: print("missing", e.status_code, e.body) except UnauthorizedError: print("refresh your token") ``` \== TypeScript ```ts import { HubQuantumError } from "@quantum-hub/qhub-api/quantum"; try { await client.jobs.getJob("does-not-exist"); } catch (err) { if (err instanceof HubQuantumError) { console.error(err.statusCode, err.body); } else { throw err; } } ``` ::: ## Reference ### Job | Field | Type | Description | | ---------------------------------------- | ----------------- | --------------------------------------------------- | | `id` | `str?` | Unique job identifier. | | `name` | `str?` | Optional human-readable name. | | `backend_id` | `str?` | Backend the job runs on. | | `provider` | `JobProvider?` | Provider enum (see below). | | `provider_job_id` | `str?` | Provider-assigned job id. | | `input_params` | `JsonNode?` | Backend-specific parameters supplied at submission. | | `input_format` | `JobInputFormat?` | Format of the submitted input. | | `tags` | `List[str]?` | Free-form tags. | | `status` | `JobStatus?` | See [Jobs](#jobs). | | `created_at` / `started_at` / `ended_at` | `str?` (ISO 8601) | Lifecycle timestamps. | | `runtime` | `int?` | Runtime in milliseconds. | | `shots` | `int?` | Number of shots. | | `session_id` | `str?` | Parent session, if any. | | `sdk_provider` | `JobSdkProvider?` | SDK that produced the input. | ### Session | Field | Type | Description | | -------------------------------------------------------- | --------------------- | --------------------------------------------------------------------- | | `id` | `str?` | Unique session identifier. | | `backend_id` | `str?` | Backend the session runs on. | | `provider` | `SessionProvider?` | Provider enum. | | `status` | `SessionStatus?` | See [Sessions](#sessions). | | `mode` | `SessionMode?` | `"batch"` or `"dedicated"`. | | `created_at` / `started_at` / `closed_at` / `expires_at` | `str?` | Lifecycle timestamps. | | `usage_time_millis` | `int?` | Billed usage time. | | `provider_id` | `str?` | Provider-assigned session id. | | `tags` | `List[str]?` | Free-form tags. | | `metadata` | `JsonNode?` | Free-form metadata supplied at creation. | | `sdk_provider` | `SessionSdkProvider?` | SDK that opened the session. | | `final` | `bool?` | True once the session is in a terminal state. | | `final_not_aborted` | `bool?` | True when the session reached a terminal state without being aborted. | ### Backend | Field | Type | Description | | ----------------------- | -------------------------- | ------------------------------------------------------------------------- | | `id` | `str?` | Backend identifier (e.g. `aws.sim.sv1`). | | `internal_id` | `str?` | Hub-internal id. | | `provider` | `BackendProvider?` | AZURE, AWS, IBM, QRYD, QUDORA, QUANDELA, IQM, KIPU. | | `hardware_provider` | `BackendHardwareProvider?` | Actual hardware vendor. | | `name` / `display_name` | `str?` | Machine-readable / human-readable name. | | `type` | `BackendType?` | `QPU`, `SIMULATOR`, `ANNEALER`, `UNKNOWN`. | | `technology` | `BackendTechnology?` | `SUPERCONDUCTING`, `TRAPPED_ION`, `PHOTONIC`, `NEUTRAL_ATOMS`, `UNKNOWN`. | | `queue_size` | `int?` | Current queue length. | | `updated_at` | `str?` | Last-updated timestamp. | | `access_type` | `BackendAccessType?` | How access is billed (e.g. pay-per-use). | | `documentation` | `Documentation?` | Doc links and per-SDK guidance. | | `configuration` | `Configuration?` | Native gate set, qubit count, supported formats. | | `availability` | `List[AvailabilityTimes]?` | Operating windows. | | `costs` | `List[Cost]?` | Pricing per provider. | | `has_calibration` | `bool?` | True if calibration data is available. | | `free_of_charge` | `bool?` | True if execution is not billed. | ### Pagination wrappers All paginated endpoints return a `PageResponse*` object with the shape `{content: List[T]?, page: int?, size: int?, total_elements: int?, total_pages: int?}`. `page` is zero-based. ### Enums * `JobStatus` - `UNKNOWN | PENDING | ABORTED | RUNNING | COMPLETED | FAILED | CANCELLING | CANCELLED` * `SessionStatus` - `UNKNOWN | ABORTED | OPEN | ACTIVE | INACTIVE | DRAINING | CLOSED` * `BackendStateInfoStatus` - `UNKNOWN | ONLINE | PAUSED | OFFLINE | RETIRED` (returned by `backends.get_backend_status` / `backends.getBackendStatus`). * `SessionMode` - `batch | dedicated` * `JobInputFormat` - `OPEN_QASM_V1 | OPEN_QASM_V2 | OPEN_QASM_V3 | QIR_V1 | BRAKET_OPEN_QASM_V3 | BRAKET_AHS_PROGRAM | IONQ_CIRCUIT_V1 | QISKIT_QPY | QOQO | PERCEVAL | IQM_JOB_INPUT_V1` * `JobProvider` / `SessionProvider` / `WorkloadResponseProvider` - `AZURE | AWS | IBM | QRYD | QUDORA | QUANDELA | IQM | KIPU` * `BackendProvider` - `AZURE | AWS | IBM | QRYD | QUDORA | QUANDELA | IQM | KIPU` * `BackendHardwareProvider` - `IONQ | RIGETTI | OQC | AWS | AZURE | IBM | QUERA | IQM | QUDORA | QUANTINUUM | QUANDELA | KIPU` * `JobSdkProvider` / `SessionSdkProvider` / `WorkloadResponseSdkProvider` - `QISKIT | BRAKET | PERCEVAL | CLIENT` * `BackendType` - `QPU | SIMULATOR | ANNEALER | UNKNOWN` * `BackendTechnology` - `SUPERCONDUCTING | TRAPPED_ION | PHOTONIC | NEUTRAL_ATOMS | UNKNOWN` * `WorkloadResponseType` - `JOB | SESSION` ### Field casing Quantum-API DTOs are snake\_case in both languages (`backend_id`, `created_at`) - the generator preserves the wire casing for this API in TypeScript. TypeScript method arguments themselves use camelCase (`sessionId`, `serviceExecutionId`), but request-body shapes follow the DTO casing above. --- --- url: /sdk-api-platform.md description: >- Catalog surface for the Kipu Quantum Hub - services, applications, organizations, data pools, subscriptions, marketplace listings, and billing through the HubPlatformClient. --- # HubPlatformClient > Part of the [Kipu Quantum Hub API SDK reference](./sdk-api.md) - see the landing page for [installation](./sdk-api.md#installation) and [Python credential helpers](./sdk-api.md#python-credential-helpers). `HubPlatformClient` covers the catalog surface: services, applications, organizations, data pools, subscriptions, marketplace listings, billing, and more. The constructor has the same shape as `HubQuantumClient`, including the `organization_id` / `organizationId` option. ## Authentication `HubPlatformClient` authenticates with a personal access token sent as the `X-Auth-Token` request header. The same Python credential helpers documented under [Python credential helpers](./sdk-api.md#python-credential-helpers) work here - pass the resolved token via `api_key`. ## Organization scoping Most Platform endpoints accept an `X-OrganizationId` header to scope the request to a specific organization. `HubPlatformClient` exposes a dedicated `organization_id` / `organizationId` constructor option that sends the header on every request; in TypeScript, individual sub-client methods also accept a per-request override. Leave it unset to operate in your personal account. ## Quickstart ::: tabs key:pythonTS \== Python ```python from qhub.api.platform import HubPlatformClient platform = HubPlatformClient( api_key="YOUR_PERSONAL_ACCESS_TOKEN", organization_id="YOUR_ORGANIZATION_ID", ) services = platform.services.get_services(page=0, size=20) for service in services.content or []: print(service.id, service.display_name) ``` \== TypeScript ```ts import { HubPlatformClient } from "@quantum-hub/qhub-api/platform"; const platform = new HubPlatformClient({ apiKey: process.env.KQH_PERSONAL_ACCESS_TOKEN!, organizationId: process.env.KQH_ORGANIZATION_ID!, }); const services = await platform.services.getServices({ page: 0, size: 20 }); for (const service of services.content ?? []) { console.log(service.id, service.displayName); } ``` ::: ## Constructor | Parameter | Required | Description | |--------------------------------------|------------------|--------------------------------------------------------------------------------------------------| | `api_key` / `apiKey` | yes | Personal access token; sent as `X-Auth-Token`. | | `base_url` / `baseUrl` | no | Overrides both the default environment and `environment` if supplied. | | `environment` | no | `HubPlatformClientEnvironment.DEFAULT` (Python) / `HubPlatformEnvironment.Default` (TypeScript). | | `organization_id` / `organizationId` | no | Value for the `X-OrganizationId` header; sent on every request from this client. | | `headers` | no | Additional headers merged into every request. | | `timeout` / `timeoutInSeconds` | no | Read timeout in seconds; defaults to 60 when no custom HTTP client is supplied. | | `max_retries` / `maxRetries` | no | Number of retries for transient failures; defaults to 2. | | `follow_redirects` | no (Python only) | Passed through to `httpx.Client`; defaults to `True`. | | `httpx_client` / `fetch` | no | Inject a preconfigured HTTP client (Python) or `fetch` implementation (TypeScript). | | `logging` | no | Logger instance or `{level, logger, silent}` config dict. | Python also ships `AsyncHubPlatformClient` with the same shape plus an `httpx.AsyncClient` hook. ## Namespaces `HubPlatformClient` exposes the following sub-clients, lazily instantiated on first access. | Namespace | Purpose | |----------------------|----------------------------------------------------------------------------------| | `algorithms` | Algorithms in the catalog and their sketches, relations, and access permissions. | | `applications` | Applications (projects that consume services). | | `authentication` | Access tokens and keys (API tokens, APIM / gateway credentials). | | `billing` | Balances, budgets, credits, billing history, revenue, cost reports. | | `data_pools` | Data pools (shared datasets) and their files. | | `data_pool_grants` | Grants that let services read from a data pool. | | `data_pool_shares` | Visibility grants of data pools to users or organizations. | | `eligibility` | Entitlement and eligibility checks for the current principal. | | `external_services` | Externally-hosted services registered in the catalog. | | `git_integrations` | Git provider integrations used by services. | | `implementations` | Implementations attached to algorithms. | | `managed_services` | Managed services (Hub-hosted) including builds and source upload. | | `marketplace` | Marketplace listings for algorithms, services, implementations, use cases. | | `organizations` | Organizations you belong to, members, provider tokens. | | `quantum_workloads` | Cross-organization view of quantum workload costs (reporting surface). | | `service_executions` | Service execution records, inputs, outputs, logs, metrics, cancellation. | | `service_jobs` | Service jobs (the subset of execution records that are long-running jobs). | | `service_shares` | Visibility grants of services to users or organizations. | | `services` | Services in the catalog (the published, versioned artefacts). | | `subscriptions` | Subscriptions (for services that require opt-in). | | `use_cases` | Use cases exposed in the catalog / marketplace. | | `user_notifications` | Per-user notifications. | | `users` | Invitations, accounts, and per-user provider tokens. | | `workflow_services` | Workflow services (multi-step managed services). | User-profile data (name, email, profile image, personal access tokens) now lives on `HubUserClient` - see [HubUserClient](./sdk-api-user.md). ## Endpoints | Default base URL | Override | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `https://api.hub.kipu-quantum.com/qc-catalog` | `base_url=...` / `baseUrl: ...`, or `environment=HubPlatformClientEnvironment.DEFAULT` (Python) / `environment: HubPlatformEnvironment.Default` (TypeScript). | ## Errors All Python errors extend `ApiError` (in `qhub.api.platform.core.api_error`); all TypeScript errors extend `HubPlatformError`. | Status | Python class | TypeScript class | Meaning | | ------ | ----------------------------- | ----------------------------- | ---------------------------------------------------- | | 400 | `BadRequestError` | `BadRequestError` | Malformed request. | | 401 | `UnauthorizedError` | `UnauthorizedError` | Missing or invalid credentials. | | 403 | `ForbiddenError` | `ForbiddenError` | Authenticated but not permitted. | | 404 | `NotFoundError` | `NotFoundError` | Resource does not exist. | | 409 | `ConflictError` | `ConflictError` | Conflict with current state, e.g. publication races. | | 413 | `ContentTooLargeError` | `ContentTooLargeError` | Upload exceeds the configured size limit. | | 415 | `UnsupportedMediaTypeError` | `UnsupportedMediaTypeError` | Wrong `Content-Type` on the request body. | | 422 | `UnprocessableEntityError` | `UnprocessableEntityError` | Validation failure on the request body. | | 500 | `InternalServerError` | `InternalServerError` | Server-side failure. | Other failure modes: * **Auth misconfiguration (TypeScript)** - constructing `HubPlatformClient` without `apiKey` throws `HubPlatformError` with message `"Please provide 'apiKey' when initializing the client"`. * **Timeouts** - hitting `timeoutInSeconds` raises `HubPlatformTimeoutError` in TypeScript; in Python, timeouts surface as the underlying `httpx.TimeoutException`. All HTTP error instances expose `status_code`, `headers`, and `body` (Python) / `statusCode`, `rawResponse`, and `body` (TypeScript). ## Reference ### Field casing Platform-API DTOs use snake\_case in Python (`created_at`, `display_name`) and camelCase in TypeScript (`createdAt`, `displayName`). ### Pagination wrappers Identical to the Quantum API - `{content, page, size, total_elements, total_pages}`, zero-based `page`. --- --- url: /sdk-api-service.md description: >- Invoke deployed services through the Kipu Quantum Hub gateway with the HubServiceClient - start executions, stream logs, and download result files using short-lived bearer tokens. --- # HubServiceClient > Part of the [Kipu Quantum Hub API SDK reference](./sdk-api.md) - see the landing page for [installation](./sdk-api.md#installation) and [Python credential helpers](./sdk-api.md#python-credential-helpers). `HubServiceClient` talks to deployed services through the service gateway. It takes a short-lived bearer `token` and, in Python, the async client additionally accepts `async_token: Callable[[], Awaitable[str]]` for async-driven refresh. The default environment is a URL template - `https://gateway.hub.kipu-quantum.com///` - that you **must** override by passing the concrete gateway URL for the service you are invoking (the placeholders are not substituted by the SDK). All operations live under the `service_api` (Python) / `serviceApi` (TypeScript) namespace. ## Authentication The bearer token is sent as `Authorization: Bearer `. Tokens are short-lived; the caller is responsible for rotating them before they expire. In Python, pass either a `str` or a `Callable[[], str]` to `token`; the async client also accepts `async_token: Callable[[], Awaitable[str]]` for refresh that involves async I/O. Python users can resolve tokens with the [Python credential helpers](./sdk-api.md#python-credential-helpers) and pass the value via `token`. ## Quickstart Start a service execution and stream its logs. ::: tabs key:pythonTS \== Python ```python from qhub.api.service import HubServiceClient service = HubServiceClient( base_url="https://gateway.hub.kipu-quantum.com/acme/vqe/v1", token="YOUR_GATEWAY_TOKEN", ) execution = service.service_api.start_execution( request={"input": {"molecule": "H2"}}, ) for entry in service.service_api.get_logs(execution.id) or []: print(entry.timestamp, entry.severity, entry.message) ``` \== TypeScript ```ts import { HubServiceClient } from "@quantum-hub/qhub-api/service"; const service = new HubServiceClient({ baseUrl: "https://gateway.hub.kipu-quantum.com/acme/vqe/v1", token: "YOUR_GATEWAY_TOKEN", }); const execution = await service.serviceApi.startExecution({ input: { molecule: "H2" }, }); const logs = (await service.serviceApi.getLogs(execution.id)) ?? []; for (const entry of logs) { console.log(entry.timestamp, entry.severity, entry.message); } ``` ::: ## Constructor | Parameter | Required | Description | | ------------------------------ | ----------------- | ----------------------------------------------------------------------------------------------------------------- | | `token` | yes | Bearer token, or a callable returning one. Sent as `Authorization: Bearer `. | | `async_token` (Python async only) | no | Awaitable callable for async-driven token refresh; used instead of `token` on async requests when supplied. | | `base_url` / `baseUrl` | yes (effectively) | Concrete gateway URL for the service; the default `environment` is a template that must be overridden. | | `environment` | no | `HubServiceClientEnvironment.DEFAULT` (Python) / `HubServiceEnvironment.Default` (TypeScript) - template URL. | | `headers` | no | Additional headers merged into every request. | | `timeout` / `timeoutInSeconds` | no | Read timeout in seconds; defaults to 60 when no custom HTTP client is supplied. | | `max_retries` / `maxRetries` | no | Number of retries for transient failures; defaults to 2. | | `follow_redirects` | no (Python only) | Passed through to `httpx.Client`; defaults to `True`. | | `httpx_client` / `fetch` | no | Inject a preconfigured HTTP client (Python) or `fetch` implementation (TypeScript). | | `logging` | no | Logger instance or `{level, logger, silent}` config dict. | Python also ships `AsyncHubServiceClient` with the same shape plus an `httpx.AsyncClient` hook and the `async_token` callable. ## Behaviour * The default `environment` is an un-substituted template, so `base_url` / `baseUrl` is effectively required - see [Endpoints](#endpoints). * `base_url` / `baseUrl` always wins over `environment`. * `async_token` exists only on `AsyncHubServiceClient`; when supplied it is used instead of the synchronous `token` for async requests. * `max_retries` applies to transient failures (network errors and 5xx responses); per-request overrides in `request_options` (Python) or per-call options (TypeScript) take precedence. ## Service Execution lifecycle ``` PENDING ──► RUNNING ──► SUCCEEDED │ │ │ ├──► FAILED │ └──► CANCELLED └──► CANCELLED ``` `UNKNOWN` covers states the server has not yet resolved. Terminal states are `SUCCEEDED`, `FAILED`, and `CANCELLED`. ## Operations All methods live under the `service_api` (Python) / `serviceApi` (TypeScript) namespace. | Method | Description | | ------------------------------------------- | ------------------------------------------------------------------- | | `service_api.get_service_executions()` | List executions started through this gateway. | | `service_api.start_execution(request=body)` | Start a new execution; body is a JSON `dict` (reserved keys below). | | `service_api.get_status(id)` | Lightweight `ServiceExecution` snapshot. | | `service_api.get_result(id)` | Result response (HAL-style: `_links` + `_embedded`). | | `service_api.get_result_file(id, file)` | Download a single result file as a byte stream. | | `service_api.get_logs(id)` | Chronological list of log entries (oldest first). | | `service_api.cancel_execution(id)` | Request cancellation of a pending or running execution. | TypeScript exposes the same methods in camelCase (`serviceApi.getServiceExecutions()`, `serviceApi.startExecution({ ... })`, `serviceApi.getStatus(id)`, `serviceApi.getResult(id)`, `serviceApi.getResultFile(id, file)`, `serviceApi.getLogs(id)`, `serviceApi.cancelExecution(id)`). ### get\_service\_executions List every service execution started through this gateway. This method takes no arguments beyond per-request options. ::: tabs key:pythonTS \== Python ```python executions = service.service_api.get_service_executions() for execution in executions: print(execution.id, execution.status) ``` \== TypeScript ```ts const executions = await service.serviceApi.getServiceExecutions(); for (const execution of executions) { console.log(execution.id, execution.status); } ``` ::: Returns `List[ServiceExecution]` / `ServiceExecution[]` - see [ServiceExecution](#serviceexecution). ### start\_execution Start a service execution, processed asynchronously. The returned `ServiceExecution` starts non-terminal; poll [get\_status](#get_status) until it reaches a terminal state, then read the result with [get\_result](#get_result). | Parameter | Required | Type | Default | Description | | --------- | -------- | ------------------------------------------ | ------- | ----------------------------------------------------------------- | | `request` | yes | `RequestBody` (`Dict[str, Any]` / `Record`) | — | Free-form JSON body; two keys are reserved (see [Reserved request keys](#reserved-request-keys)). | In Python `request` is keyword-only; in TypeScript the body is passed as the first positional argument. ::: tabs key:pythonTS \== Python ```python execution = service.service_api.start_execution( request={ "input": {"molecule": "H2", "basis": "sto-3g"}, "tags": ["demo"], }, ) print(execution.id, execution.status) ``` \== TypeScript ```ts const execution = await service.serviceApi.startExecution({ input: { molecule: "H2", basis: "sto-3g" }, tags: ["demo"], }); console.log(execution.id, execution.status); ``` ::: Returns `ServiceExecution` - see [ServiceExecution](#serviceexecution). ### get\_status Lightweight snapshot of a single execution's lifecycle state. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------------------------- | | `id` | yes | `str` / `string` | — | The id of a service execution. | ::: tabs key:pythonTS \== Python ```python execution = service.service_api.get_status(execution_id) print(execution.status) ``` \== TypeScript ```ts const execution = await service.serviceApi.getStatus(executionId); console.log(execution.status); ``` ::: Returns `ServiceExecution`; inspect its `status` (a [ServiceExecutionStatus](#enums)) against the [lifecycle](#service-execution-lifecycle). ### get\_result Retrieve the result of a service execution as a HAL envelope. The response carries `_embedded.status` (a full [ServiceExecution](#serviceexecution)) and `_links.status` (a [HalLink](#resultresponse) pointing back at the status resource); individual result files are downloaded with [get\_result\_file](#get_result_file). | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------------------------- | | `id` | yes | `str` / `string` | — | The id of a service execution. | ::: tabs key:pythonTS \== Python ```python result = service.service_api.get_result(execution_id) if result.embedded and result.embedded.status: print(result.embedded.status.status) ``` \== TypeScript ```ts const result = await service.serviceApi.getResult(executionId); console.log(result._embedded?.status?.status); ``` ::: Returns `ResultResponse` - see [ResultResponse](#resultresponse). ### get\_result\_file Download a single named result file as a byte stream. Use this for large outputs rather than parsing the full result into memory. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------------------------- | | `id` | yes | `str` / `string` | — | The id of a service execution. | | `file` | yes | `str` / `string` | — | The name of the result file. | ::: tabs key:pythonTS \== Python ```python with open("result.bin", "wb") as out: for chunk in service.service_api.get_result_file(execution_id, "result.bin"): out.write(chunk) ``` \== TypeScript ```ts import { Writable } from "node:stream"; import { createWriteStream } from "node:fs"; const file = await service.serviceApi.getResultFile(executionId, "result.bin"); await file.stream().pipeTo(Writable.toWeb(createWriteStream("result.bin"))); ``` ::: Returns `Iterator[bytes]` (Python; `AsyncIterator[bytes]` on the async client) / `BinaryResponse` (TypeScript). ### get\_logs Chronological list of log entries for an execution, oldest first. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------------------------- | | `id` | yes | `str` / `string` | — | The id of a service execution. | ::: tabs key:pythonTS \== Python ```python for entry in service.service_api.get_logs(execution_id) or []: print(entry.timestamp, entry.severity, entry.message) ``` \== TypeScript ```ts const logs = (await service.serviceApi.getLogs(executionId)) ?? []; for (const entry of logs) { console.log(entry.timestamp, entry.severity, entry.message); } ``` ::: Returns `Optional[List[LogEntry]]` / `LogEntry[] | null | undefined` - see [LogEntry](#logentry). Each entry's `severity` is a [LogEntrySeverity](#enums); the response may be absent when no logs have been emitted yet. ### cancel\_execution Request cancellation of a pending or running execution. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------------------------- | | `id` | yes | `str` / `string` | — | The id of a service execution. | ::: tabs key:pythonTS \== Python ```python service.service_api.cancel_execution(execution_id) ``` \== TypeScript ```ts await service.serviceApi.cancelExecution(executionId); ``` ::: No body returned. ## Reserved request keys `start_execution` takes a free-form JSON body, but two top-level keys are reserved by the gateway: * `input` - the service-specific payload; every deployed service expects this shape. * `inputDataRefs` - references to mounted data-pool files, used when the input is too large to inline or lives in a shared data pool. Everything else in the body is forwarded verbatim to the service. ## Advanced usage ### Polling for completion ::: tabs key:pythonTS \== Python ```python import time TERMINAL = {"SUCCEEDED", "FAILED", "CANCELLED"} def wait(execution_id: str, poll_interval: float = 2.0) -> str: while True: status = service.service_api.get_status(execution_id).status if status in TERMINAL: return status time.sleep(poll_interval) final = wait(execution.id) if final == "SUCCEEDED": result = service.service_api.get_result(execution.id) ``` \== TypeScript ```ts const TERMINAL = new Set(["SUCCEEDED", "FAILED", "CANCELLED"]); async function wait(executionId: string, pollMs = 2000): Promise { for (;;) { const { status } = await service.serviceApi.getStatus(executionId); if (status && TERMINAL.has(status)) return status; await new Promise((r) => setTimeout(r, pollMs)); } } const final = await wait(execution.id); if (final === "SUCCEEDED") { const result = await service.serviceApi.getResult(execution.id); } ``` ::: ### Streaming a result file Large result files are better consumed as a byte stream than loaded whole. ::: tabs key:pythonTS \== Python ```python with open("result.json", "wb") as out: for chunk in service.service_api.get_result_file(execution.id, "result.json"): out.write(chunk) ``` \== TypeScript ```ts import { Writable } from "node:stream"; import { createWriteStream } from "node:fs"; const file = await service.serviceApi.getResultFile(execution.id, "result.json"); await file.stream().pipeTo(Writable.toWeb(createWriteStream("result.json"))); ``` ::: ## Endpoints | Default base URL | Override | | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `https://gateway.hub.kipu-quantum.com///` | **Always override** - the template placeholders are not substituted by the SDK; pass the full concrete URL via `base_url` / `baseUrl`. | ## Errors Service gateway responses do not surface per-status typed exceptions. All Python errors extend `ApiError` (in `qhub.api.service.core.api_error`); all TypeScript errors extend `HubServiceError`. Other failure modes: * **Auth misconfiguration (TypeScript)** - constructing `HubServiceClient` without `token` throws `HubServiceError` with message `"Please provide 'token' when initializing the client"`. * **Timeouts** - hitting `timeoutInSeconds` raises `HubServiceTimeoutError` in TypeScript; in Python, timeouts surface as the underlying `httpx.TimeoutException`. * **Missing base URL** - because the default environment is an un-substituted template, calling any method without providing a concrete `base_url` / `baseUrl` will produce a 404 (or DNS) failure against the literal URL `https://gateway.hub.kipu-quantum.com///`. In Python, omitting both `base_url` and `environment` raises `Exception("Please pass in either base_url or environment to construct the client")`. All HTTP error instances expose `status_code`, `headers`, and `body` (Python) / `statusCode`, `rawResponse`, and `body` (TypeScript). ## Reference ### ServiceExecution | Field | Python / wire name | Type | Description | | --------------------- | ----------------------------------------------- | ------------------------ | ------------------------------------- | | id | `id` | `str` | Service execution identifier. | | created\_at | `created_at` / `createdAt` | `str` | When the execution was created. | | started\_at | `started_at` / `startedAt` | `str?` | When processing started. | | ended\_at | `ended_at` / `endedAt` | `str?` | When processing ended. | | status | `status` | `ServiceExecutionStatus` | See [lifecycle](#service-execution-lifecycle). | | type | `type` | `ServiceExecutionType?` | `MANAGED` or `WORKFLOW`. | | service\_id | `service_id` / `serviceId` | `str?` | Catalog service this run belongs to. | | service\_definition\_id | `service_definition_id` / `serviceDefinitionId` | `str?` | Service definition (version). | | application\_id | `application_id` / `applicationId` | `str?` | Application that invoked the service. | | tags | `tags` | `List[str]?` | Free-form tags. | ### ResultResponse HAL envelope returned by [get\_result](#get_result). Unknown top-level keys are preserved in both languages. | Field | Python / wire name | Type | Description | | -------- | ------------------------- | ------------------------- | ---------------------------------------------------- | | embedded | `embedded` / `_embedded` | `ResultResponseEmbedded?` | `status` holds a full [ServiceExecution](#serviceexecution). | | links | `links` / `_links` | `ResultResponseLinks?` | `status` holds a `HalLink` to the status resource. | `HalLink` fields: `href` (`str`, required), and optional `templated` (`bool?`), `type` (`str?`), `deprecation` (`str?`), `name` (`str?`), `profile` (`str?`), `title` (`str?`), `hreflang` (`str?`). ### LogEntry | Field | Python / wire name | Type | Description | | --------- | ------------------ | ---------------------- | ---------------------------------------------------------------- | | message | `message` | `str` | Log message content. | | severity | `severity` | `LogEntrySeverity?` | One of `DEBUG`, `NOTICE`, `INFO`, `WARNING`, `ERROR`. | | timestamp | `timestamp` | `datetime` / `string` | When the entry was logged; a `datetime` in Python, a `string` in TypeScript. | ### RequestBody The body accepted by [start\_execution](#start_execution). A free-form JSON object: `Dict[str, Any]` (Python) / `Record` (TypeScript). See [Reserved request keys](#reserved-request-keys) for the two top-level keys the gateway interprets. ### Enums * `ServiceExecutionStatus` - `UNKNOWN | PENDING | RUNNING | SUCCEEDED | CANCELLED | FAILED` * `ServiceExecutionType` - `MANAGED | WORKFLOW` * `LogEntrySeverity` - `DEBUG | NOTICE | INFO | WARNING | ERROR` ### Field casing Service-API DTOs use snake\_case in Python and camelCase in TypeScript for most fields, but preserve the HAL wire names (`_links`, `_embedded`) on `ResultResponse` in both languages. --- --- url: /sdk-api-user.md description: >- Read user profiles, manage personal access tokens, resolve user identities, and search the user directory with the HubUserClient. --- # HubUserClient > Part of the [Kipu Quantum Hub API SDK reference](./sdk-api.md) - see the landing page for [installation](./sdk-api.md#installation) and [Python credential helpers](./sdk-api.md#python-credential-helpers). `HubUserClient` talks to the user-service. Use it to read the currently-authenticated user's profile, manage personal access tokens, resolve user identities, and search the user directory. ## Authentication `HubUserClient` authenticates with a personal access token sent as the `X-Auth-Token` request header. The same Python credential helpers documented under [Python credential helpers](./sdk-api.md#python-credential-helpers) work here - pass the resolved token via `api_key`. The `users`, `user_registration_status`, and `authentication` namespaces are public/registration-oriented and do not require an authenticated principal; the remaining namespaces operate on the current user and do. ## Quickstart Fetch the current user and list their personal access tokens. ::: tabs key:pythonTS \== Python ```python from qhub.api.user import HubUserClient user = HubUserClient(api_key="YOUR_PERSONAL_ACCESS_TOKEN") me = user.user_settings.get_current_user() tokens = user.user_settings_personal_access_tokens.get_personal_access_tokens() print(me.email, [t.name for t in tokens.access_tokens or []]) ``` \== TypeScript ```ts import { HubUserClient } from "@quantum-hub/qhub-api/user"; const user = new HubUserClient({ apiKey: process.env.KQH_PERSONAL_ACCESS_TOKEN!, }); const me = await user.userSettings.getCurrentUser(); const tokens = await user.userSettingsPersonalAccessTokens.getPersonalAccessTokens(); console.log(me.email, (tokens.accessTokens ?? []).map((t) => t.name)); ``` ::: ## Constructor | Parameter | Required | Description | | ------------------------------ | ---------------- | ------------------------------------------------------------------------------------ | | `api_key` / `apiKey` | yes | Personal access token; sent as `X-Auth-Token`. | | `base_url` / `baseUrl` | no | Overrides both the default environment and `environment` if supplied. | | `environment` | no | `HubUserClientEnvironment.DEFAULT` (Python) / `HubUserEnvironment.Default` (TypeScript). | | `headers` | no | Additional headers merged into every request. | | `timeout` / `timeoutInSeconds` | no | Read timeout in seconds; defaults to 60 when no custom HTTP client is supplied. | | `max_retries` / `maxRetries` | no | Number of retries for transient failures; defaults to 2. | | `follow_redirects` | no (Python only) | Passed through to `httpx.Client`; defaults to `True`. | | `httpx_client` / `fetch` | no | Inject a preconfigured HTTP client (Python) or `fetch` implementation (TypeScript). | | `logging` | no | Logger instance or `{level, logger, silent}` config dict. | Python also ships `AsyncHubUserClient` with the same shape plus an `httpx.AsyncClient` hook. ## Behaviour * `base_url` / `baseUrl` always wins over `environment`; omitting both in Python raises `Exception("Please pass in either base_url or environment to construct the client")`. * [check\_user\_exists](#check_user_exists) is a `HEAD` existence check: it returns the response headers (`Dict[str, str]` in Python, a `Headers` object in TypeScript) when the user exists and raises `NotFoundError` (404) otherwise - treat a thrown `NotFoundError`, not a falsy return, as "does not exist". * [get\_user\_profile\_image](#get_user_profile_image) returns a nullable string (`Optional[str]` / `string | undefined`), not raw bytes; the value is absent when the user has no profile image. * A personal access token's raw `value` is returned only once, at creation or default-token regeneration; subsequent reads omit it. ## Namespaces | Namespace | Purpose | | -------------------------------------- | ---------------------------------------------------------------------------------- | | `user_settings` | Current-user profile: get, update, delete, manage profile image. | | `user_settings_personal_access_tokens` | List, create, delete, and regenerate personal access tokens for the current user. | | `authentication` | Resolve the principal for a personal access token. | | `users` | Look up users by id, check existence, fetch profile images. | | `user_registration_status` | Read the registration status of a user by email. | | `search` | Search the user directory. | TypeScript exposes the same namespaces in camelCase (`userSettings`, `userSettingsPersonalAccessTokens`, `authentication`, `users`, `userRegistrationStatus`, `search`). ## Current user profile Operations on the currently-authenticated user, under the `user_settings` (Python) / `userSettings` (TypeScript) namespace. | Method | Description | | ------------------------------------------------------------ | -------------------------------------------- | | `user_settings.get_current_user()` | Full profile of the current user. | | `user_settings.update_current_user(current_position=?, homepage=?, about=?)` | Update editable profile fields. | | `user_settings.delete_current_user()` | Permanently delete the current user. | | `user_settings.update_current_user_profile_image(file=...)` | Upload or replace the profile image. | | `user_settings.delete_current_user_profile_image()` | Delete the profile image. | ### get\_current\_user Return the full profile of the currently-authenticated user. If the user does not yet exist in the user-service database, it is provisioned just-in-time from the identity provider and then returned. This method takes no arguments beyond per-request options. ::: tabs key:pythonTS \== Python ```python me = user.user_settings.get_current_user() print(me.id, me.email, me.firstname, me.lastname) ``` \== TypeScript ```ts const me = await user.userSettings.getCurrentUser(); console.log(me.id, me.email, me.firstname, me.lastname); ``` ::: Returns `UserDto` - see [UserDto](#userdto). ### update\_current\_user Update the editable profile fields. Core identity fields (email, first name, last name) are owned by the identity provider and cannot be changed here. | Parameter | Required | Type | Default | Description | | ------------------ | -------- | ------------------ | ------- | --------------------------------------------------- | | `current_position` | no | `str?` / `string?` | — | Current professional position or role. | | `homepage` | no | `str?` / `string?` | — | Personal or professional homepage (fully-qualified URL). | | `about` | no | `str?` / `string?` | — | Short biography shown on the profile. | ::: tabs key:pythonTS \== Python ```python me = user.user_settings.update_current_user( current_position="Quantum Engineer", homepage="https://example.com", about="Working on variational algorithms.", ) ``` \== TypeScript ```ts const me = await user.userSettings.updateCurrentUser({ currentPosition: "Quantum Engineer", homepage: "https://example.com", about: "Working on variational algorithms.", }); ``` ::: Returns `UserDto` with the updated values applied. ### delete\_current\_user Permanently delete the current user together with all associated resources (personal access tokens, profile image, profile metadata), and remove the user from the identity provider. The operation cannot be undone. This method takes no arguments beyond per-request options. ::: tabs key:pythonTS \== Python ```python user.user_settings.delete_current_user() ``` \== TypeScript ```ts await user.userSettings.deleteCurrentUser(); ``` ::: Responds with `204 No Content`; no body is returned. ### update\_current\_user\_profile\_image Upload or replace the current user's profile image. The image is sent as a multipart form part named `file`. | Parameter | Required | Type | Default | Description | | --------- | -------- | ------------------------------------- | ------- | -------------------------- | | `file` | yes | `core.File` / `core.file.Uploadable` | — | The profile image to upload. | ::: tabs key:pythonTS \== Python ```python with open("avatar.png", "rb") as f: user.user_settings.update_current_user_profile_image(file=f) ``` \== TypeScript ```ts import { createReadStream } from "node:fs"; await user.userSettings.updateCurrentUserProfileImage({ file: createReadStream("avatar.png"), }); ``` ::: Responds with `204 No Content`; no body is returned. ### delete\_current\_user\_profile\_image Remove the current user's profile image. Idempotent - succeeds even when no image is set. This method takes no arguments beyond per-request options. ::: tabs key:pythonTS \== Python ```python user.user_settings.delete_current_user_profile_image() ``` \== TypeScript ```ts await user.userSettings.deleteCurrentUserProfileImage(); ``` ::: Responds with `204 No Content`; no body is returned. ## Personal access tokens Manage the current user's personal access tokens (PATs), under the `user_settings_personal_access_tokens` (Python) / `userSettingsPersonalAccessTokens` (TypeScript) namespace. | Method | Description | | --------------------------------------------------------------------- | ---------------------------------------------------- | | `user_settings_personal_access_tokens.get_personal_access_tokens()` | List all PATs plus the default token value. | | `user_settings_personal_access_tokens.create_personal_access_token(name=..., expires_at=?)` | Create a new PAT; raw value returned once. | | `user_settings_personal_access_tokens.delete_personal_access_token(id)` | Delete a PAT by id. | | `user_settings_personal_access_tokens.regenerate_default_personal_access_token()` | Rotate the default PAT. | ### get\_personal\_access\_tokens List all personal access tokens for the current user, together with the raw value of the current default token. The raw `value` of individual tokens is omitted on reads. This method takes no arguments beyond per-request options. ::: tabs key:pythonTS \== Python ```python tokens = user.user_settings_personal_access_tokens.get_personal_access_tokens() for token in tokens.access_tokens or []: print(token.id, token.name, token.expires_at) ``` \== TypeScript ```ts const tokens = await user.userSettingsPersonalAccessTokens.getPersonalAccessTokens(); for (const token of tokens.accessTokens ?? []) { console.log(token.id, token.name, token.expiresAt); } ``` ::: Returns `AccessTokensDto` - see [AccessTokensDto](#accesstokensdto). ### create\_personal\_access\_token Create a new personal access token. The raw `value` is returned exactly once, on this response; store it immediately. | Parameter | Required | Type | Default | Description | | ------------ | -------- | ------------------ | ------- | ------------------------------------------------ | | `name` | yes | `str` / `string` | — | Human-readable token name. | | `expires_at` | no | `str?` / `string?` | — | Expiry date as `yyyy-MM-dd`; omit for no expiry. | ::: tabs key:pythonTS \== Python ```python token = user.user_settings_personal_access_tokens.create_personal_access_token( name="ci-token", expires_at="2027-01-01", ) print(token.value) # shown only here ``` \== TypeScript ```ts const token = await user.userSettingsPersonalAccessTokens.createPersonalAccessToken({ name: "ci-token", expiresAt: "2027-01-01", }); console.log(token.value); // shown only here ``` ::: Returns `AccessTokenDto` - see [AccessTokenDto](#accesstokendto). ### delete\_personal\_access\_token Permanently delete a personal access token by id. The token must belong to the calling user. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------------------------- | | `id` | yes | `str` / `string` | — | Id of the token to delete. | ::: tabs key:pythonTS \== Python ```python user.user_settings_personal_access_tokens.delete_personal_access_token(token_id) ``` \== TypeScript ```ts await user.userSettingsPersonalAccessTokens.deletePersonalAccessToken(tokenId); ``` ::: Responds with `204 No Content`; no body is returned. ### regenerate\_default\_personal\_access\_token Rotate the default personal access token and return its new raw value. The previous default value is invalidated. This method takes no arguments beyond per-request options. ::: tabs key:pythonTS \== Python ```python result = user.user_settings_personal_access_tokens.regenerate_default_personal_access_token() print(result.default_token) # shown only here ``` \== TypeScript ```ts const result = await user.userSettingsPersonalAccessTokens.regenerateDefaultPersonalAccessToken(); console.log(result.defaultToken); // shown only here ``` ::: Returns `DefaultAccessTokenDto` - see [DefaultAccessTokenDto](#defaultaccesstokendto). ## Principal resolution Resolve a personal access token into an authenticated principal, under the `authentication` namespace. This is an internal/gateway-oriented endpoint: it validates the PAT carried in the `X-Auth-Token` header and returns the resolved user. ### authorize\_by\_personal\_access\_token Validate the current token and return the authenticated principal (user id plus token metadata). This method takes no arguments beyond per-request options. ::: tabs key:pythonTS \== Python ```python principal = user.authentication.authorize_by_personal_access_token() print(principal.id, principal.access_token.name if principal.access_token else None) ``` \== TypeScript ```ts const principal = await user.authentication.authorizeByPersonalAccessToken(); console.log(principal.id, principal.accessToken?.name); ``` ::: Returns `PersonalAccessTokenPrincipal` - see [PersonalAccessTokenPrincipal](#personalaccesstokenprincipal). ## User lookup Public read access to user profiles and profile images, under the `users` namespace. | Method | Description | | ----------------------------------- | -------------------------------------------- | | `users.get_user_by_id(id)` | Public overview of a user. | | `users.check_user_exists(id)` | `HEAD`-style existence check. | | `users.get_user_profile_image(id)` | Fetch a user's profile image. | ### get\_user\_by\_id Return a public overview (id, first name, last name) of a user. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | User identifier. | ::: tabs key:pythonTS \== Python ```python overview = user.users.get_user_by_id(user_id) print(overview.firstname, overview.lastname) ``` \== TypeScript ```ts const overview = await user.users.getUserById(userId); console.log(overview.firstname, overview.lastname); ``` ::: Returns `UserOverviewDto` - see [UserOverviewDto](#useroverviewdto). ### check\_user\_exists `HEAD`-style existence check for a user. Returns the response headers when the user exists and raises `NotFoundError` (404) otherwise - see [Behaviour](#behaviour). | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | User identifier. | ::: tabs key:pythonTS \== Python ```python from qhub.api.user.errors import NotFoundError try: user.users.check_user_exists(user_id) exists = True except NotFoundError: exists = False ``` \== TypeScript ```ts import { NotFoundError } from "@quantum-hub/qhub-api/user"; let exists = true; try { await user.users.checkUserExists(userId); } catch (err) { if (err instanceof NotFoundError) exists = false; else throw err; } ``` ::: Returns the response headers (`Dict[str, str]` / `Headers`); no body is emitted. ### get\_user\_profile\_image Fetch a user's profile image. Returns `None` / `undefined` when the user has no image; raises `NotFoundError` (404) when the user does not exist. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | --------------- | | `id` | yes | `str` / `string` | — | User identifier. | ::: tabs key:pythonTS \== Python ```python image = user.users.get_user_profile_image(user_id) ``` \== TypeScript ```ts const image = await user.users.getUserProfileImage(userId); ``` ::: Returns `Optional[str]` / `string | undefined` - see [Behaviour](#behaviour) for the nullable-string contract. ## User registration status Read the registration state of a user identified by email, under the `user_registration_status` (Python) / `userRegistrationStatus` (TypeScript) namespace. ### get\_user\_registration\_status Return the registration status for an email address; drives the registration flow. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | ------------------------------------- | | `email` | yes | `str` / `string` | — | Email address to query (sent as a query parameter). | ::: tabs key:pythonTS \== Python ```python status = user.user_registration_status.get_user_registration_status( email="someone@example.com", ) print(status.status, status.message) ``` \== TypeScript ```ts const status = await user.userRegistrationStatus.getUserRegistrationStatus({ email: "someone@example.com", }); console.log(status.status, status.message); ``` ::: Returns `RegistrationResponse` - see [RegistrationResponse](#registrationresponse). ## User search Full-text search across user profiles, under the `search` namespace. ### users Case-insensitive substring search over first name, last name, username, and email. | Parameter | Required | Type | Default | Description | | --------- | -------- | ---------------- | ------- | ------------------------------------------ | | `q` | yes | `str` / `string` | — | Search query (sent as a query parameter). | ::: tabs key:pythonTS \== Python ```python results = user.search.users(q="alice") for match in results: print(match.id, match.firstname, match.lastname) ``` \== TypeScript ```ts const results = await user.search.users({ q: "alice" }); for (const match of results) { console.log(match.id, match.firstname, match.lastname); } ``` ::: Returns a plain (possibly empty) `List[UserSearchDto]` / `UserSearchDto[]` - there is no pagination wrapper. See [UserSearchDto](#usersearchdto). ## Endpoints | Default base URL | Override | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `https://api.hub.kipu-quantum.com/user-service` | `base_url=...` / `baseUrl: ...`, or `environment=HubUserClientEnvironment.DEFAULT` (Python) / `environment: HubUserEnvironment.Default` (TypeScript). | ## Errors All Python errors extend `ApiError` (in `qhub.api.user.core.api_error`); all TypeScript errors extend `HubUserError`. | Status | Python class | TypeScript class | Meaning | | ------ | --------------------- | --------------------- | ------------------------------------ | | 400 | `BadRequestError` | `BadRequestError` | Malformed request. | | 401 | `UnauthorizedError` | `UnauthorizedError` | Missing or invalid credentials. | | 403 | `ForbiddenError` | `ForbiddenError` | Authenticated but not permitted. | | 404 | `NotFoundError` | `NotFoundError` | Resource does not exist. | Other failure modes: * **Auth misconfiguration (TypeScript)** - constructing `HubUserClient` without `apiKey` throws `HubUserError` with message `"Please provide 'apiKey' when initializing the client"`. * **Timeouts** - hitting `timeoutInSeconds` raises `HubUserTimeoutError` in TypeScript; in Python, timeouts surface as the underlying `httpx.TimeoutException`. All HTTP error instances expose `status_code`, `headers`, and `body` (Python) / `statusCode`, `rawResponse`, and `body` (TypeScript). ## Reference ### UserDto Full profile of the currently-authenticated user. | Field | Python / wire name | Type | Description | | ---------------- | ------------------------------- | ------ | -------------------------------------------- | | id | `id` | `str?` | Unique id (identity-provider subject). | | username | `username` | `str?` | Login name (typically the email). | | email | `email` | `str?` | Email address (identity-provider managed). | | firstname | `firstname` | `str?` | First (given) name. | | lastname | `lastname` | `str?` | Last (family) name. | | current\_position | `current_position` / `currentPosition` | `str?` | Current professional position or role. | | homepage | `homepage` | `str?` | Personal or professional homepage. | | about | `about` | `str?` | Short biography. | ### AccessTokenDto A personal access token belonging to a user. | Field | Python / wire name | Type | Description | | ---------- | ------------------------- | ------ | ---------------------------------------------------------- | | id | `id` | `str?` | Unique id of the token. | | name | `name` | `str` | Human-readable token name. | | created\_at | `created_at` / `createdAt` | `str?` | Creation timestamp (`yyyy-MM-dd HH:mm:ss`, UTC). | | used\_at | `used_at` / `usedAt` | `str?` | Last-used timestamp; `null` if never used. | | expires\_at | `expires_at` / `expiresAt` | `str?` | Expiry (`yyyy-MM-dd`); `null` means never expires. | | value | `value` | `str?` | Raw token value; returned only once at creation, `null` on reads. | ### AccessTokensDto Container for a user's personal access tokens plus the raw default-token value. | Field | Python / wire name | Type | Description | | ------------- | ---------------------------- | ------------------- | ------------------------------------ | | default\_token | `default_token` / `defaultToken` | `str?` | Raw value of the current default PAT. | | access\_tokens | `access_tokens` / `accessTokens` | `List[AccessTokenDto]?` | All PATs for the user; may be empty. | ### DefaultAccessTokenDto Response wrapping a newly-generated default personal access token value. | Field | Python / wire name | Type | Description | | ------------- | ---------------------------- | ------ | -------------------------------------------------- | | default\_token | `default_token` / `defaultToken` | `str?` | Raw value of the regenerated default PAT (returned once). | ### PersonalAccessTokenPrincipal Authenticated principal resolved from a personal access token. | Field | Python / wire name | Type | Description | | ------------ | ---------------------------- | ----------------- | ---------------------------------------------------- | | id | `id` | `str?` | Unique id of the authenticated user. | | access\_token | `access_token` / `accessToken` | `AccessTokenDto?` | Metadata of the PAT used to authenticate; raw `value` never included. | ### RegistrationResponse Registration status of a user, used by the registration flow. | Field | Python / wire name | Type | Description | | ------- | ------------------ | ---------------------------- | -------------------------------------------- | | status | `status` | `RegistrationResponseStatus?` | Current registration status (see [Enums](#enums)). | | message | `message` | `str?` | Human-readable message for the status. | ### UserOverviewDto Minimal public representation of a user. | Field | Python / wire name | Type | Description | | --------- | ------------------ | ------ | --------------------- | | id | `id` | `str?` | Unique id. | | firstname | `firstname` | `str?` | First (given) name. | | lastname | `lastname` | `str?` | Last (family) name. | ### UserSearchDto Entry in the user-search result list (returned as a bare array). | Field | Python / wire name | Type | Description | | --------- | ------------------ | ------ | --------------------- | | id | `id` | `str?` | Unique id of the match. | | firstname | `firstname` | `str?` | First (given) name. | | lastname | `lastname` | `str?` | Last (family) name. | ### Enums * `RegistrationResponseStatus` - `PENDING | APPROVED | REJECTED | NOT_FOUND | EMAIL_NOT_VERIFIED` ### Field casing User-API DTOs use snake\_case in Python and camelCase in TypeScript. Single-word fields (`id`, `email`, `firstname`, `lastname`, `username`, `name`, `value`, `homepage`, `about`, `status`, `message`) are identical across both; the aliased fields are `currentPosition`, `createdAt`, `usedAt`, `expiresAt`, `defaultToken`, `accessTokens`, and `accessToken`. --- --- url: /qhub-json-reference.md description: >- Reference for the qhub.json service configuration file, including supported fields for name, resources, runtime, and GPU allocation. --- # `qhub.json` Reference The `qhub.json` file contains your service configuration and is used by the QHubCtl CLI to deploy and run your service. It will be generated automatically by qhubctl and must be located in the root folder of your project. Here is an example containing all supported fields: ```json { "name": "my-service", "descriptionFile": "README.md", "resources": { "cpu": 2, "memory": 4, "gpu": { "type": "NVIDIA_TESLA_T4", "count": 1 } }, "runtime": "PYTHON_TEMPLATE", "serviceId": "99487f0b-21f0-4256-8335-5179d416dbb4" } ``` The following properties are supported: | Property | Type | Description | |-----------------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` | `string` | **Required**. The name of your service. | | `descriptionFile` | `string` | The name of a markdown file used as the description for your service. The file must be located in the root folder of your project. | | `resources` | `object` | **Required**. The resource configuration of your service. | | `resources.cpu` | `number` | **Required**. The number of virtual CPU cores to allocate for your service. | | `resources.memory` | `number` | **Required**. The amount of memory in GB to allocate for your service. | | `resources.gpu` | `object` | The GPU configuration of your service. | | `resources.gpu.type` | `string` | The type of GPU to allocate for your service. One of `NVIDIA_TESLA_T4` or `NVIDIA_TESLA_V100`. | | `resources.gpu.count` | `number` | The number of GPUs to allocate for your service. | | `runtime` | `string` | **Required**. The runtime to use for your service. Choose `PYTHON_TEMPLATE` to run quantum services based on our Python starter templates. Choose `DOCKER` to run custom docker images in any programming language. | | `serviceId` | `string` | References a deployed service. Gets automatically added on a successful deployment, i.e., after `qhubctl up`. | --- --- url: /references/markdown-latex-editor.md description: >- Reference for Markdown, KaTeX-powered LaTeX, QuanTikz circuits, image scaling, and sketch embedding in platform text editors. --- ### Markdown & LaTeX Since a lot of people love math and many algorithms require formulae for a better understanding, we support Markdown combined with Latex for most of the textboxes on our platform. Just use `$ latex $` for inline or `$$ latex $$` for centered equations. You can see other supported Markdown options whenever you are editing a textbox by clicking on the ?-symbol in the top right corner of the box. We use KaTeX to display LaTeX. Click [here](https://katex.org/docs/supported.html) to see the supported KaTeX features. ### Scale Image If an image is referenced in Markdown, you have the option to scale it using the following syntax: ```md ![](){width=, height=} ``` Any standard CSS unit for width is supported, but we recommend using one of the following: `px`, `em`, `rem` or `%`. ### QuanTikz Sometimes math is just not enough to express certain parts of quantum algorithms which is why we also support [quantikz](https://ctan.org/pkg/quantikz) within latex math mode. So in order to draw circuits just type ```md $$ \begin{quantikz} *cool circuit* \end{quantikz} $$ ``` ### Sketches When formulae and circuits fail to convey information there is another: Images! You can include any standard picture format (such as .png or .jpg) as a sketch by scrolling down to the bottom of the page within the details view of the algorithm and click on the green + sign at the top right corner in the "Sketches" section. After uploading it, you should see your picture within this section, as well as an ID below it. You can use this ID to include it within your description of the algorithm by the common Markdown syntax (the title does not affect the display of the image at all) ``` ![title](*image-id*) ``` --- --- url: /services/orchestration/example.md description: >- Walkthrough combining a stock time series service and a covariance generator into a single orchestrated workflow on Kipu Quantum Hub. --- # A Workflow Example In this chapter, the set up of a workflow is described along a concrete example. The example consist of 2 service calls whereas the second service processes the result of teh first service call. ## The Combined Services For this example 2 services will be combined in a workflow: * stock time series service * covariance generator Both services have to be available, published and subscribed by an application. ### Stock Time Series Service This service downloads the values of a given stock for a given time window. It takes as input data: | Input Value | Description | |---------------|--------------------------------------------------------------------------------------------------| | stock\_symbols | Array of stock names, e.g. `["IBM","AAPL"]` | | start\_date | The day of the first stock value to be retrieved. Must be in form yyyy-mm-dd, e.g., `2022-01-01` | | end\_date | The day of the last stock value to be retrieved. Must be in form yyyy-mm-dd, e.g., `2022-02-02` | It takes as input params: * no params It returns as output: * A Json structure containing for each stock an array of date-value pairs. ### Covariance Generator This service computes the covariance within an array of values. It takes as input data: | Input Value | Description | |------------------|------------------------------------------------------------| | time\_series\_data | A map of objects which contains a map of time/value pairs. | It takes as input params: | Input Value | Description | |----------------------|-------------------------------------------------------------| | covariance\_estimator | name of algorithm to be taken for computation of covariance | | number\_of\_decimals | number of ... | It returns as output: * A Json structure containing the shrunk covariance matrix. ## The Workflow Create a new workflow service. Go to the details page and open the workflow model editor. Create the following flow: by doing the following steps: * drag 2 service nodes into the editor * assign the 2 services to the nodes * click on the *change type* icon, open the list of *Platform Service Tasks* and select the proper service * connect the nodes (start node -> service node -> service node -> end node) * drag 4 data object nodes into the editor * connect the 4 data object nodes to the service nodes as shown in picture There is nothing to configure for the service nodes, but we need to configure the input/output data of the data object nodes. Configure the first data object node (in\_stock\_data): * Click on the first data object node. On the right side of the editor you find the context menu for this node. * Enter "in\_stock\_data" as name of the node. * Each data node must have a name. The name should not have white spaces within. * Click on the plus-button of the content param. Enter `start_date` as name and `${wf_sd}` as value. With this configuration you can later pass the start date as parameter `wf_sd` to the workflow. * Click again on the plus-button of the content param. Enter `end_date` as name and `${wf_ed}` as value. With this configuration you can later pass the end date as parameter `wf_ed` to the workflow. * Click again on the plus-button of the content param. Enter `stock_symbols` as name and `${wf_stsy}` as value. With this configuration you can later pass the array of stock symbols as parameter `wf_stsy` to the workflow. Configure the second data object node (out\_stock\_data): * Click on the second data object node to open the context menu for this node. * Enter "out\_stock\_data" as name of the node. * Leave the content mapping blank. * Click on the connection line to the covariance service and change the type of the connection to "transformation association". * In the context menu of this line, click on the plus-button of the Expressions param. Enter `time_series_data` as name and `${out_stock_data.jsonPath("$").element()}` as value. With this configuration the result of the stock series service, which is a json-object, will be parsed and the content will transformed into the time\_series\_data parameter which is needed by the covariance service. Configure the 3rd data object node (params\_cov): * Click on the 3rd data object node to open the context menu for this node. * Enter "params\_cov" as name of the node. * Click on the plus-button of the content param. Enter `params` as name and `{"covariance_estimator": "LedoitWolf","number_of_decimals": 4}` as value. We now have hard coded the configuration for the service how to compute the covariance result. Configure the 4th data object node (out\_covariance): * Click on the 4th data object node to open the context menu for this node. * Enter "out\_covariance" as name of the node. * Leave the content mapping blank. ## Save, Publish, and Subscribe At the end, click on the save button to store the edited workflow in your workflow service. ::: warning NOTE There is no automatic save or intermediate save, so be aware to not lose you edit work by just leaving the editor without saving your work. ::: When the workflow is ready, it has to be deployed to the deployed to the workflow engine. Click on the *Deploy button*. Now you can leave the editor and go back to the details page of your service. To trigger an execution of the workflow, the service has to be published (internal or to marketplace). After it is published you can create an application and subscribe to this service. ## Start the Service Now you can trigger an execution of the workflow via the OpenAPI-UI of your application. As the POST call expects some input params, you have to pass a structure, containing the 3 configured parameter `wf_stsy`, `wf_sd` and `wf_ed`. This is an example of how the request body may look like: ```json { "data": { "wf_stsy": { "value": "[\"IBM\",\"AAPL\"]", "type": "String" }, "wf_sd": { "value": "2022-01-02", "type": "String" }, "wf_ed": { "value": "2022-01-15", "type": "String" } } } ``` If the workflow was executed without errors you can request the result via the GET/{id}/result endpoint which you find further below in the OpenAPI-UI. The result contains an array of variables. Within this array, not only the output variables are listed, but also all input parameters and all intermediate data which are created during the workflow execution. For this example, the result should contain: | variable name | description | example content | |------------------|------------------------------------------------------------------------------------------|-----------------------------------------------------------------------| | wf\_stsy | the stock symbols for the first service passed as input param to the workflow execution | `["IBM","AAPL"]` | | wf\_st | the start date for the first service passed as input param to the workflow execution | `2022-01-01` | | wf\_ed | the end date for the first service passed as input param to the workflow execution | `2022-01-05` | | out\_stock\_data | the result of the first service | "AAPL": {"2022-01-01": 179,"2022-01-01": 180}... | | time\_series\_data | the transformed out\_stock\_data which is then passed as input param to the second service | "AAPL": {"2022-01-01": 179,"2022-01-01": 180}... | | out\_covariance | the result of the second service | "shrunk\_cov\_matrix": {"AAPL": {"AAPL":0.001},...} | --- --- url: /tutorials/tutorial-ibmq.md description: >- Build a service that uses qiskit-ibm-provider to generate random numbers on the least busy IBM Quantum Platform backend. --- # Access IBM Quantum Platform Backends in a Service This tutorial shows how to access backends offered by the IBM Quantum Platform from within a service. As an example, the code will generate some random numbers on the least busy IBM Quantum Platform backend. ## Bootstrap Project 1. Install the [CLI](../cli-reference). 2. Create a new project using `qhubctl init` and select the `Starter` template. 3. Open the project in your IDE of choice, e.g., VSCode. ## Create Python Environment Add Qiskt and the IBM Provider SDK as a dependency to your project by adding `qiskit` and `qiskit-ibm-provider` to the `requirements.txt` file in the root folder. You can now set up a Python environment using Conda: ```bash conda env create -f environment.yml conda activate ``` Conda and the `environment.yml` file are used by the platform at runtime. However, if you do not have Conda installed on your local computer, you are also able to initialize a Python virtual environment using the tooling of your choice, e.g., `pyenv` or `venv`. You are now able to run the Python `src` folder as module from your console: ```bash python3 -m src ``` ## Extend Project Open the `program.py` in your IDE. The `run()` method is the main handler function and the entry point for your program. The method takes two arguments: (1) a `data` dictionary and (2) a `params` dictionary holding the input submitted by the user. The platform [translates](../services/managed/runtime-interface) the Service API body/payload in the form of `{ "data": { }, "params": { } }` into these parameters. It is also important that the `run()` method returns a JSON serializable `Response` object. The template makes use of the classes `ResultResponse` and `ErrorResponse`. It's recommended that you use these classes as well. Next, remove the whole code from within the `run()` method. Add some required import statements: ```python from qiskit import QuantumCircuit, transpile from qiskit_ibm_provider import IBMProvider, least_busy from qiskit_ibm_provider.job import job_monitor ``` Add the following code to the `run()` method: ```python # defines the range of random numbers between 0 and 2^n_bits - 1 n_bits = data.get('n_bits', 2) token = os.getenv('QISKIT_IBM_TOKEN', None) provider = IBMProvider(token=token) devices = provider.backends(simulator=False, operational=True) backend = least_busy(devices) circuit = QuantumCircuit(n_bits, n_bits) circuit.h(range(n_bits)) circuit.measure(range(n_bits), range(n_bits)) circuit = transpile(circuit, backend) start_time = time.time() job = backend.run(circuit, shots=1000) job_monitor(job) execution_time = time.time() - start_time random_number = int(list(job.result().get_counts().keys())[0], 2) ``` The code first instantiates the `IBMProvider` using an API token value from the environment variable `QISKIT_IBM_TOKEN`. Next, we determine the least busy backend, create a simple circuit, and execute the problem by calling `backend.run()`. The `job_monitor()` function waits till the job has been completed, which is when we can extract a random number out of the job result. Finally, return some result: ```python result = { "random_number": random_number, } metadata = { "execution_time": round(execution_time, 3), } return ResultResponse(result=result, metadata=metadata) ``` ::: details Source Code (program.py) The full project can be found in our [Starter Implementation](https://dashboard.hub.kipu-quantum.com/community/implementations/1a0ae675-4b23-405c-af8e-f4189ff14e0f). ```python import os import time from typing import Dict, Any, Union from qiskit import QuantumCircuit, transpile from qiskit_ibm_provider import IBMProvider, least_busy from qiskit_ibm_provider.job import job_monitor from .libs.return_objects import ResultResponse, ErrorResponse def run(data: Dict[str, Any] = None, params: Dict[str, Any] = None) -> Union[ResultResponse, ErrorResponse]: # defines the range of random numbers between 0 and 2^n_bits - 1 n_bits = data.get('n_bits', 2) token = os.getenv('QISKIT_IBM_TOKEN', None) provider = IBMProvider(token=token) devices = provider.backends(simulator=False, operational=True) backend = least_busy(devices) circuit = QuantumCircuit(n_bits, n_bits) circuit.h(range(n_bits)) circuit.measure(range(n_bits), range(n_bits)) circuit = transpile(circuit, backend) start_time = time.time() job = backend.run(circuit, shots=1000) job_monitor(job) execution_time = time.time() - start_time random_number = int(list(job.result().get_counts().keys())[0], 2) result = { "random_number": random_number, } metadata = { "execution_time": round(execution_time, 3), } return ResultResponse(result=result, metadata=metadata) ``` ::: ## Run the Project Locally Run the program using `QISKIT_IBM_TOKEN=9356f0193daa... python3 -m src` (copy the API token value from your IBM Quantum Platform account settings). The output should be similar to the following: ```shell {"result": {"random_number": 235}, "metadata": {"execution_time": 2810.306}} ``` The project, or the `__main__.py` respectively, uses the `data.json` and the `params.json` as input for the `run()` when executed locally. You may experiment with different inputs of the `n_bits` input data parameter. The next section shows how to create and run a service using the code you just have written. ## Create a Service We use the CLI to create a new service in your personal account. Login with the CLI: ```shell qhubctl login -t ``` Create the service: ```shell qhubctl up ``` After a while, the console should print something similar like this: ``` Pushing Image (2/2)... Service created 🚀 ``` Congratulations. You have successfully created a service. Before you can execute the service, a few more steps are necessary: 1. Add your IBM Quantum API token in the Provider Access Tokens [settings](https://dashboard.hub.kipu-quantum.com/settings) of your account. 2. On the [service overview page](https://dashboard.hub.kipu-quantum.com/services), open your service and go to the Runtime Configuration (`Edit Service > Runtime Configuration`). Activate the option `Add secrets to runtime environment`. This option lets the platform inject your API token to the execution runtime. The value is made available through the environment variable `QISKIT_IBM_TOKEN`. In your code, you already instrumented the `IBMProvider` accordingly whenever this environment variable is present. ## Run your Service Using the CLI, you can quickly run a Service Job: ```shell qhubctl run ``` The `run` command uses the `data.json` and `params.json` file as input for the job. You may adjust the values accordingly. Alternatively, you could have created a Service Job through the platform UI. More information about Jobs and how to use them can be found in our [documentation](../services/managed/jobs). Furthermore, you could also *publish your service for internal use* and read on how to use the service utilizing Applications. Just follow the steps in the [Using a Service](../services/using-a-service) section in our documentation. --- --- url: /services/workflow/introduction.md description: >- Introduction to BPMN workflow fundamentals for orchestrating multi-step quantum services visually on Kipu Quantum Hub. --- # BPMN Workflow Tutorial Welcome to the complete guide for creating and managing BPMN workflows on the Quantum Hub! This tutorial will take you from the basics to building complex quantum workflows that orchestrate multiple services. ### What You'll Learn By the end of this tutorial, you'll be able to: * Understand BPMN fundamentals and workflow concepts * Create your first quantum workflow service on the Quantum Hub * Use the visual workflow modeler effectively * Design both sequential and parallel service execution * Implement proper data flow between services * Create reusable workflow APIs * Follow best practices for production workflows ### Why Use Workflows? Workflows solve a key challenge in quantum computing: \*\*orchestrating complex, multistep processes \*\* without manual programming. Instead of writing Python code to integrate individual platform services, you can: ✅ **Visually design** your process flow using BPMN diagrams\ ✅ **Automate service execution** with built-in error handling\ ✅ **Handle long-running processes** (hours to weeks) reliably\ ✅ **Monitor progress** in real-time\ ✅ **Reuse workflows** as standalone platform services\ ✅ **Bridge technical and business requirements** for better collaboration ### Prerequisites Before starting this tutorial: * Be familiar with basic quantum computing concepts * Have subscriptions to the platform services you want to orchestrate If you want to follow the examples in this tutorial, you can subscribe to the following services: * [MQT Benchmarking](https://hub.kipu-quantum.com/marketplace/services/aa08ae7d-c593-442a-9c8d-f961d5a87b00) * [IonQ Simulator](https://hub.kipu-quantum.com/marketplace/services/fcb63fad-b09e-4cd7-bc33-58690157e3c9) * [MQT Simulator](https://hub.kipu-quantum.com/marketplace/services/5b71eb55-7f1f-4f6e-af17-b6d050b26d86) * [E-Mail Service](https://hub.kipu-quantum.com/marketplace/services/b4697587-f67c-4ea4-ad7f-6dc2145aa5d7) To subscribe to these services, create a new application on the [platform > applications](https://dashboard.hub.kipu-quantum.com/applications). You can create subscriptions to the services by selecting your new application when clicking the `Subscribe` button on the details page of the respective service in the marketplace. ## Part 1: Understanding BPMN Basics Before diving into workflow creation, let's understand the fundamentals of BPMN (Business Process Model and Notation). ### What is BPMN? BPMN is a standardized visual language for modeling business processes. In the platform, we use BPMN 2.0 to define how quantum services should be executed and how data flows between them. #### Key BPMN Elements for Platform Workflows | Element | Symbol | Icon | Purpose | |---------------------------|--------|----------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------| | **Start Event** | ○ | | Marks where your workflow begins | | **End Event** | ● | | Marks where your workflow ends and specifies which variables it returns to the caller | | **Platform Service Task** | ▢ | | Executes a subscribed platform service | | **Parallel Gateway** | ◇+ | | Splits flow to run tasks in parallel | | **Sequence Flow** | → | → | Shows the order of execution | #### Example: Simple Sequential Workflow ``` ○ → [Generate Circuit] → [Execute on Backend] → [Send Results] → ● ``` #### Example: Parallel Execution Workflow ``` ○ → [Generate Circuit] → ◇+ → [Backend 1] → ◇ → [Send Results] → ● └ → [Backend 2] → ┘ ``` ## Part 2: Creating Your First Workflow Service ### Step 1: Create the Service 1. Navigate to the [service creation page](https://dashboard.hub.kipu-quantum.com/services/new) 2. Select **"Create New Service"** and choose **"Quantum Workflow Service"**. 3. Fill in the required information: | Property | Description | Example | |--------------|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Name | Choose a meaningful name for your service | "Quantum Benchmarking Workflow" | | Service Type | Select "Quantum Workflow Service" | Quantum Workflow Service | | Summary | Brief description for the marketplace | "Automated benchmarking across multiple quantum backends" | | Description | Detailed explanation of what your workflow does | "This workflow generates benchmark circuits, executes them on multiple quantum computing backends in parallel, and sends results via email notification." | 4. Click **"Create Service"** to proceed ### Step 2: Access the Workflow Modeler 1. Click on your newly created workflow service 2. Navigate to the **"Workflow"** tab 3. You'll see the visual workflow modeler with a single **Start Event** (○) ## Part 3: Building Your First Workflow - Practical Example Let's build a real quantum benchmarking workflow step by step. This workflow will: 1. Generate a benchmarking circuit 2. Execute it on multiple quantum backends in parallel 3. Send results via email when complete ### Understanding the Workflow Modeler Interface In the following image, you can see the main components of the workflow modeler: ![Workflow Modeler Interface](./workflow_modeler_interface.png) #### The Canvas * **Central workspace** where you design your workflow * Initially shows only a **Start Event** (○) * **Drag and drop** elements from the palette to build your workflow #### The Palette (Left Side) * Contains all BPMN elements you can use * **Common elements** are visible by default * Click **"..."** for advanced elements (loops, conditional flows, etc.) #### Essential Elements for Workflow Creation * **Platform Service Task** (▢): Calls a subscribed platform service * **Parallel Gateway** (◇+): Creates parallel execution paths * **End Event** (●): Marks workflow completion * **Sequence Flow** (→): Connects elements to show execution order #### The Properties Panel (Right Side) * Displays configuration options for the selected element * Use it to set properties like service names, input/output variables, and more * Click on any element to see its properties here ### Step-by-Step: Building the Control Flow #### Step 1: Add Your First Service Task 1. From the **palette**, drag a **Platform Service Task** (▢) onto the canvas 2. Position it to the right of the Start Event 3. **Connect** the Start Event to the task: * Click on the **Start Event** (○) * Drag the appearing **arrow** to the **Platform Service Task** #### Step 2: Configure the Service Task 1. Click the **wrench icon** (🔧) on the task 2. In the configuration panel, select the `MQT Benchmarking` service from the dropdown. This service will generate a quantum circuit for benchmarking. 3. In the properties panel on the right, select the General tab. 4. Give the task a descriptive name, e.g., "Generate Benchmark Circuit", instead of the default name for more clarity. #### Step 3: Add Parallel Execution For running the circuit on multiple backends simultaneously: 1. **Add a Parallel Gateway**: * Drag **Parallel Gateway** (◇+) from the palette * Place it after your benchmarking task * Connect the benchmarking task → parallel gateway 2. **Add Backend Execution Tasks**: * Drag two **Platform Service Tasks** for different backends * Position them below the parallel gateway * Connect the parallel gateway to each task: * Click the **parallel gateway** * Drag arrows to each task * Configure each task to use a different backend service by clicking the wrench icon (🔧) and selecting the appropriate service: * For the first task, select `IonQ Simulator Execution Service` * For the second task, select `mqt-ddsim` and give it a name like "MQT Simulator" 3. **Add Synchronization Gateway**: * Drag another **Parallel Gateway** after the backend tasks * Connect both backend tasks to this synchronization gateway * This ensures both backends complete before continuing #### Step 4: Add Final Steps 1. **Add Email Notification**: * Drag a **Platform Service Task** after the synchronization gateway * Configure it to use an email notification service * Name it "Send Results Email" 2. **Add End Event**: * Drag an **End Event** (●) from the palette * Connect the email task to the end event 3. **Save Your Workflow**: * Click the **Save** button in the top left #### Your Complete Workflow Should Look Like: ``` ○ → [Generate Circuit] → ◇+ → [IonQ Simulator] → ◇+ → [Send Email] → ● └ -→ [MQT Execution] → ┘ ``` ![Control Flow Example](./result-workflow.png) ### Step-by-Step: Implementing Data Flow Data flow defines how information (like quantum circuits, results, parameters) moves between services in your workflow. #### Understanding Data Variables Each service in your workflow can: * **Receive input data** from previous steps * **Produce output data** for following steps * **Access workflow parameters** (like email addresses, benchmark names) #### Step 1: Define Workflow Input Parameters When you want your workflow to receive input from external sources (API calls), you must configure the \*\*Start Event \*\* with a manual request example that defines the expected input structure. The corresponding OpenAPI specification will be generated automatically based on this configuration. 1. In the workflow modeler, select the Start Event (○) 2. In the properties panel, navigate to the **"API Description"** section 3. Define an example input your workflow will accept ```json { "benchmark_name": "ghz", "email_recipient": "your-email@your-company.domain" } ``` This manual request example serves multiple purposes: * **Each key in the JSON** corresponds to a workflow variable * **Documents the API interface** for external systems * **Provides examples** for testing and integration The field names from your manual request example become workflow variables that can be referenced directly in service task configurations. For example, `benchmark_name` and `email_recipient` can be used in subsequent service tasks to pass the data as input. #### Step 2: Configure Service Task Data Mapping When you select a service task in your workflow, you need to configure how data flows in and out. Click the service task to open the properties panel on the right. ![Service Task Data Mapping](./dataflow-input.png) To provide input and output data for the service task, you can use [FEEL expressions](https://docs.camunda.io/docs/components/modeler/feel/what-is-feel/). #### Setting Up Input Variables 1. Select the "Generate Benchmark Circuit" service task. 2. **Navigate to the "Inputs" section** in the properties panel. 3. **Add input variables** by clicking the `+` **button**. 4. **Define the "local variable name"**. This is maps to the service's request elements that it expects as input. Use the exact names as shown in the API documentation of the service interface. You can see the API example values in the **Request Example** panel. In the example shown in the figure above, the only expected input of the service is `data` as printed in the Request Example panel. Thus, use `data` as the local variable name to provide a value for the service. 5. **Set values** to the variable using JSON format. The expected structure is shown in the API documentation and Request Example panel. For the MQT Benchmarking service, you could set the value of the `data` variable like this: ```json { "benchmark_name": "qhz", "circuit_size": 5 } ``` 6. **Use workflow variables** directly (without quotes) when referencing data from previous steps. For example, if you have a variable `benchmark_name` from any previous step, you can use it like this: ```json { "benchmark_name": benchmark_name, "circuit_size": 5 } ``` This will automatically set the value from the output variable called `benchmark_name` within the workflow into the JSON object when the service is invoked. The key points: * **Local variable name**: `data` (as shown in the service's interface documentation) * **Variable assignment value**: Maps your input variables to the service's expected input. * **Use the Request Example**: The panel shows a "Request Example" - structure your input mapping to match this format. * **Variable references**: Use workflow variable names directly (without quotes) when referencing data from previous steps. #### Setting Up Output Variables 1. **Navigate to the "Outputs" section** in the service task configuration. 2. **Define how to store the service response** in workflow variables. 3. **Use the Response Example**: The panel shows a "Response Example" - this tells you what data structure the service will return. In the example shown below, the generated circuit from the MQT Benchmarking service is returned in the `qasm` field of the response. It is stored this in a variable called `Circuit` for use in subsequent tasks. ![Service Task Output Mapping](./dataflow-output.png) #### Practical Data Mapping Examples `MQT Benchmarking` Service Configuration **Inputs section:** * Local variable name: `data` * Variable assignment value: ```json { "benchmark_name": benchmark_name, "circuit_size": 5 } ``` **Outputs section:** * Store the service response (QASM circuit) in variable: `quantum_circuit` `IonQ Execution` Service Configuration\ **Inputs section:** * Local variable name: `data` * Variable assignment value: ```json { "circuit": Circuit } ``` **Outputs section:** * Store the execution results in variable: `IonqExecutionResult` from the `counts` value of the service's response. `MQT Simulator` Service Configuration\ **Inputs section:** * Local variable name: `data` with variable assignment value: ```json { "qc": Circuit } ``` * Local variable name: `params` with variable assignment value: ```json { "shots": 100 } ``` **Outputs section:** * Store the execution results in variable: `MqtExecutionResult` from the `result.counts` value of the service's response. `Email Service` Configuration **Inputs section:** * Local variable name: `data` with variable assignment value: ```json { "to": email_recipient, "subject": "Benchmark result available", "message": "I'm happy to inform you that the backend execution results are available: IonQ: " + string(IonqExecutionResult) + " MQT DDSIM " + string(MqtExecutionResult) } ``` **Key Tips:** * ✅ **Always check the Request/Response Examples** in the service task configuration * ✅ **Match the exact JSON structure** shown in the Request Example * ✅ **Use workflow variables** (like `benchmark`, `email_recipient`) in your mappings * ✅ **Store service outputs** in descriptive variable names for use by later tasks * ❌ **Don't guess the data format** - always refer to the provided examples #### Step 3: Returning Data from Workflows By default, workflow services do not return any data via the API to external callers. To return data, configure output variables on the **End Event**: 1. Select the End Event (●) in your workflow 2. Navigate to the "Outputs" section in the properties panel 3. Add output variables using [FEEL expressions](./data-manipulation.md) The output variables become fields in your workflow's API response. For example, if you configure: * Variable name: `ionq_result` with expression: `IonqExecutionResult` * Variable name: `mqt_result` with expression: `MqtExecutionResult` * Variable name: `status` with literal expression: `"completed"` Your API will return: ```json { "ionq_result": {"00": 45, "11": 55}, "mqt_result": {"00": 42, "11": 58}, "status": "completed" } ``` #### Advanced Data Flow Patterns ##### Conditional Data Flow Use **Exclusive Gateways** (◇×) to route data based on conditions: ``` [Check Results] → ◇× → [Success Path] (if results valid) └→ [Error Path] (if results invalid) ``` ### Creating a Reusable Service API Transform your workflow into a reusable service that others can integrate: #### Step 1: Define Service Interface In the **"API Description"** section of your workflow, create a meaningful example input. Currently, no schema is supported but only the example request. #### Step 2: Implement Error Handling Attach an **Error Boundary Event** to any platform service task whose failure should be recovered rather than surfaced to the API caller. The modeler auto-wires it to the platform's `SERVICE_FAILED` error and captures the failure payload into a per-host `_serviceError` process variable (named by the host task's id) for the recovery path to consume. See [Handling Errors in Workflow Services](./error-handling.md) for the full catch-event contract, how to read the failure payload with FEEL, and what API consumers see when an error escapes uncaught. ## Part 4: Best Practices & Advanced Patterns ### Workflow Design Best Practices #### 1. Keep It Simple ✅ **Do**: Start with simple sequential workflows\ ✅ **Do**: Add complexity gradually as needed\ ❌ **Avoid**: Over-engineering with unnecessary parallel paths #### 2. Handle Errors Gracefully ✅ **Do**: Add timeout boundaries to long-running tasks\ ✅ **Do**: Implement retry logic for critical services\ ✅ **Do**: Always notify users of failures\ ❌ **Avoid**: Silent failures or infinite loops #### 3. Design for Monitoring ✅ **Do**: Use descriptive task names\ ✅ **Do**: Add intermediate checkpoints for long workflows\ ✅ **Do**: Log important intermediate results\ ❌ **Avoid**: Black-box workflows without visibility #### 4. Optimize for Performance ✅ **Do**: Use parallel execution for independent tasks\ ✅ **Do**: Minimize data transformations between services\ ✅ **Do**: Cache expensive computations when possible\ ❌ **Avoid**: Unnecessary sequential bottlenecks ### Common Workflow Patterns #### Pattern 1: Fan-Out/Fan-In (Parallel Processing) ``` ○ → [Prepare Data] → ◇+ → [Process A] → ◇+ → [Combine Results] → ● └ → [Process B] → ┘ ``` **Use when**: Processing the same data with multiple services #### Pattern 2: Pipeline (Sequential Processing) ``` ○ → [Step 1] → [Step 2] → [Step 3] → [Step 4] → ● ``` **Use when**: Each step depends on the previous step's output #### Pattern 3: Conditional Flow ``` ○ → [Check Condition] → ◇× → [Path A] → ● └→ [Path B] → ● ``` **Use when**: Different actions needed based on data or conditions #### Pattern 4: Error Handling with Compensation ``` ○ → [Main Task] → [Success Action] → ● │ └→ [Error Handler] → [Cleanup] → ● ``` **Use when**: You need to clean up after failures ### Troubleshooting Common Issues #### Issue: "Service Not Found" **Symptoms**: Workflow fails with service subscription error\ **Solutions**: 1. Verify you have an active subscription to the service 2. Check service name spelling in task configuration 3. Ensure service is still available in the marketplace #### Issue: "Data Mapping Error" **Symptoms**: Service receives wrong data format\ **Solutions**: 1. Check input/output variable names match exactly 2. Verify data types match service requirements 3. Add data transformation script if needed #### Issue: "Workflow Timeout" **Symptoms**: Workflow stops without completion\ **Solutions**: 1. Check individual service timeouts 2. Add timer boundary events to long-running tasks 3. Implement progress checkpoints #### Issue: "Parallel Tasks Don't Synchronize" **Symptoms**: Some parallel branches complete but others hang\ **Solutions**: 1. Ensure all parallel paths have proper error handling 2. Add timeout boundaries to parallel tasks 3. Check for data dependency conflicts ### Testing Your Workflows #### 1. Start Simple * Test individual service tasks first * Use minimal test data initially * Verify each step completes successfully #### 2. Test Error Scenarios * Simulate service failures * Test timeout conditions * Verify error notifications work #### 3. Performance Testing * Test with realistic data sizes * Monitor execution times * Check resource usage #### 4. Integration Testing * Test the complete end-to-end flow * Verify all data mappings work correctly * Test with real user scenarios ### Production Deployment Checklist Before publishing your workflow service: #### ✅ Functionality * \[ ] All service tasks configured correctly * \[ ] Data flow works end-to-end * \[ ] Error handling implemented * \[ ] User notifications working #### ✅ Documentation * \[ ] Service description clear and accurate * \[ ] API parameters documented * \[ ] Expected outputs defined * \[ ] Usage examples provided #### ✅ Performance * \[ ] Reasonable execution timeouts set * \[ ] Parallel execution used where beneficial * \[ ] Resource usage optimized #### ✅ Reliability * \[ ] Error handling tested * \[ ] Retry logic implemented * \[ ] Monitoring and logging configured ### Advanced Features #### Looping and Iteration Use **Loop Characteristics** on tasks to repeat operations: * **Standard Loop**: Repeat a fixed number of times * **Multi-Instance**: Process array data in parallel * **Sequential Multi-Instance**: Process array data one by one #### Conditional Logic Use **Exclusive Gateways** (◇×) with conditions: ```javascript // Example condition: Check if fidelity is acceptable $ {ionq_results.fidelity > 0.95} ``` #### Event-Driven Workflows Use **Message Events** to trigger workflows from external systems: * **Message Start Event**: Start workflow from API call * **Message Intermediate Event**: Wait for external notification * **Timer Events**: Schedule periodic executions #### Sub-Processes Break complex workflows into reusable sub-processes: * **Embedded Sub-Process**: Inline complex logic * **Call Activity**: Reuse other workflow definitions ### Next Steps Now that you understand BPMN workflows on the Quantum Hub, it's time to start building your own!: 1. **Practice**: Build simple workflows with your subscribed services 2. **Experiment**: Try different patterns (parallel, conditional, loops) 3. **Share**: Publish useful workflows for the community 4. **Learn More**: Explore advanced BPMN features as needed ### Resources * [BPMN 2.0 Specification](https://www.omg.org/spec/BPMN/2.0/) * [Service Marketplace](https://dashboard.hub.kipu-quantum.com/marketplace) * [Community Support Forum (Discord)](https://discord.gg/qhwDBPpuFE) --- --- url: /cli-reference.md --- # CLI Reference > Generated from `@quantum-hub/qhubctl@2.1.0`. Run `npm run docs:generate` to update. ## Overview ``` qhubctl [options] ``` | Command | Description | |---------|-------------| | [`qhubctl compress`](#compress) | Compresses the current project and creates a ZIP file. Use a '.qhubignore' file (gitignore syntax) to exclude files and directories from the ZIP file | | [`qhubctl datapool upload`](#datapool-upload) | Upload files to a data pool | | [`qhubctl get-context`](#get-context) | Get the current context, i.e., the personal or organization account you are currently working with | | [`qhubctl init`](#init) | Bootstrap project to create a service | | [`qhubctl list-contexts`](#list-contexts) | Retrieves the available contexts, i.e., the personal or organizational accounts available to you to work with | | [`qhubctl login`](#login) | Login with your credentials | | [`qhubctl logout`](#logout) | Logout | | [`qhubctl openapi`](#openapi) | Generates the OpenAPI description for your project based on the parameter and return types of your run() method. The output of this command will be used when creating or updating your service. Supports PYTHON\_TEMPLATE (with src/program.py) and DOCKER runtime (with explicit entrypoint). Requires uv and a valid uv project in the workspace. The "qhub-commons" dependency is installed/upgraded automatically as a dependency | | [`qhubctl run`](#run) | Creates a job execution | | [`qhubctl serve`](#serve) | Runs your project locally via qhub-serve inside the workspace virtual environment, exposing the same HTTP endpoints as Kipu Quantum Hub (start execution, check status, cancel, retrieve results) | | [`qhubctl services build-logs`](#services-build-logs) | Show the build logs of a service | | [`qhubctl services build-status`](#services-build-status) | Show the build status of a service | | [`qhubctl services list`](#services-list) | List all services of the current context | | [`qhubctl set-context`](#set-context) | Set the current context, i.e., the personal or organization account you are currently working with | | [`qhubctl up`](#up) | Creates or updates a service | ## compress Compresses the current project and creates a ZIP file. Use a '.qhubignore' file (gitignore syntax) to exclude files and directories from the ZIP file. `.git`, `node_modules`, `.venv`, `__pycache__`, and `service.zip` are always excluded. Because `.qhubignore` uses gitignore syntax, re-including a directory's contents requires both the directory and its contents — for example, to ship only `src` and the `Dockerfile`: ``` * !src !src/** !Dockerfile ``` **Usage** ``` qhubctl compress ``` ## datapool upload Upload files to a data pool **Usage** ``` qhubctl datapool upload --file ... [--datapool-id ...] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `-f, --file ` | yes | — | Path to a file to upload (can be specified multiple times) | | `-d, --datapool-id ` | no | — | ID of the data pool to upload the files to (optional - will prompt to create a new one if not provided) | ## get-context Get the current context, i.e., the personal or organization account you are currently working with. **Usage** ``` qhubctl get-context ``` ## init Bootstrap project to create a service. **Usage** ``` qhubctl init [--name ...] [--non-interactive] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `--name ` | no | — | The name of the service | | `--non-interactive` | no | — | Run it in non-interactive mode | ## list-contexts Retrieves the available contexts, i.e., the personal or organizational accounts available to you to work with. **Usage** ``` qhubctl list-contexts ``` ## login Login with your credentials **Usage** ``` qhubctl login [--token ...] [--base-path ...] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `-t, --token ` | no | — | Your personal access token | | `--base-path ` | no | — | Custom base path for authentication (development) | ## logout Logout **Usage** ``` qhubctl logout ``` ## openapi Generates the OpenAPI description for your project based on the parameter and return types of your run() method. The output of this command will be used when creating or updating your service. Supports PYTHON\_TEMPLATE (with src/program.py) and DOCKER runtime (with explicit entrypoint). Requires uv and a valid uv project in the workspace. The "qhub-commons" dependency is installed/upgraded automatically as a dependency. **Usage** ``` qhubctl openapi [--force] [--file ...] [--format ...] [--entrypoint ...] [--package ...] [--method ...] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `-f, --force` | no | — | Overwrite the output file if it already exists | | `--file ` | no | — | The file to write the OpenAPI description to | | `--format ` | no | — | The format to generate the OpenAPI description \[possible values: yaml] | | `--entrypoint ` | no | — | The entrypoint to your program in the format "package.module:function" (default: "src.program:run"). Overrides package and method flags. | | `--package ` | no | — | The package/module path to your program (e.g., "src.program" or "my\_package.main"). Only used if entrypoint is not provided. | | `--method ` | no | — | The method/function name in your program (default: "run"). Only used if entrypoint is not provided. | ## run Creates a job execution **Usage** ``` qhubctl run [] [--input ...] [--input-files ...] [--store-input] [--tag ...] [--detached] ``` **Arguments** | Argument | Required | Description | |----------|----------|-------------| | `` | no | The ID of the service to run (optional — reads from qhub.json if omitted) | **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `-i, --input ` | no | — | Input as JSON string. | | `--input-files ` | no | — | Comma-separated paths to files containing input to be merged (default: ./input/data.json,./input/params.json). | | `--store-input` | no | — | Persist the input data alongside the job on the server. | | `--tag ` | no | \`\` | Tag to attach to the job. Repeat the flag to attach multiple tags. | | `--detached` | no | — | Executes the job in detached mode, i.e., without waiting for it to finish. | ## serve Runs your project locally via qhub-serve inside the workspace virtual environment, exposing the same HTTP endpoints as Kipu Quantum Hub (start execution, check status, cancel, retrieve results). **Usage** ``` qhubctl serve [--port ...] [--log-level ...] [--workspace ...] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `-p, --port ` | no | `8081` | The port on which the local web server accepts requests | | `--log-level ` | no | `INFO` | Log level for qhub-serve. Possible values: TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL | | `-w, --workspace ` | no | — | Path to the workspace directory (must be a valid uv project). Defaults to the current directory. | ## services build-logs Show the build logs of a service. **Usage** ``` qhubctl services build-logs [--id ...] [--json] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `--id ` | no | — | The ID of the service (reads from qhub.json if omitted) | | `--json` | no | — | Output in JSON format | ## services build-status Show the build status of a service. **Usage** ``` qhubctl services build-status [--id ...] [--json] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `--id ` | no | — | The ID of the service (reads from qhub.json if omitted) | | `--json` | no | — | Output in JSON format | ## services list List all services of the current context. **Usage** ``` qhubctl services list [--json] [--name ...] [--all] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `--json` | no | — | Output in JSON format | | `--name ` | no | — | Filter services by name (case-insensitive full match) | | `--all` | no | — | Display all service properties (only available with --json) | ## set-context Set the current context, i.e., the personal or organization account you are currently working with. **Usage** ``` qhubctl set-context [] ``` **Arguments** | Argument | Required | Description | |----------|----------|-------------| | `` | no | The ID of the context to switch to | ## up Creates or updates a service. Use a '.qhubignore' file (gitignore syntax) to exclude files and directories from the ZIP file. `.git`, `node_modules`, `.venv`, `__pycache__`, and `service.zip` are always excluded. Because `.qhubignore` uses gitignore syntax, re-including a directory's contents requires both the directory and its contents — for example, to ship only `src` and the `Dockerfile`: ``` * !src !src/** !Dockerfile ``` **Usage** ``` qhubctl up [--silent] [--no-save-id] [--name ...] [--image ...] [--registry-username ...] [--registry-password ...] ``` **Options** | Flag | Required | Default | Description | |------|----------|---------|-------------| | `--silent` | no | — | Suppresses all outputs, helpful when executed in a CI/CD pipeline. | | `--no-save-id` | no | — | Prevents storing the created service ID in qhub.json file. | | `--name ` | no | — | Override the service name from qhub.json with a custom name. | | `--image ` | no | — | Deploy a pre-built image from a container registry, given as registry/repo:tag (the tag defaults to "latest"). | | `--registry-username ` | no | — | Username to pull a private registry image. Falls back to the QHUB\_REGISTRY\_USERNAME environment variable. | | `--registry-password ` | no | — | Password to pull a private registry image. Falls back to the QHUB\_REGISTRY\_PASSWORD environment variable. | --- --- url: /tutorials/tutorial-qiskit-with-platform-sdk.md description: >- Use HubQiskitProvider from the Quantum SDK to list backends and execute Qiskit circuits on quantum hardware and simulators. --- # Execute Qiskit Circuits using the Quantum SDK This tutorial describes how you can use the Quantum SDK to execute your Qiskit code on different quantum backends supported by the platform. The SDK is a wrapper for Qiskit 2.2. Hence, it provides the same functionality and syntax as the original Qiskit SDK. You can use the SDK either directly from your favorite IDE or in a [service](../services/managed/introduction). ## Install the Quantum SDK To install the Quantum SDK you need to have Python 3.11 or higher installed. The package is released on PyPI and can be installed via `pip`: ```bash pip install qhub-quantum ``` ## Create an Access Token To access the quantum backends from your Qiskit code you need to have a valid Kipu Quantum Hub account and a quantum access token. This token is used to authenticate your requests to the platform and to track the usage costs of your quantum executions. Log in to [Kipu Quantum Hub](https://dashboard.hub.kipu-quantum.com/home) and copy your personal access token to the clipboard. Optionally, you may create a dedicated access token in your user [settings](https://dashboard.hub.kipu-quantum.com/settings/access-tokens) that you can use for your Qiskit code. Copy your new token and store it in a safe place. ## Backend Selection and Execution In your Qiskit code you can access the Kipu Quantum Hub quantum backends through the `HubQiskitProvider` object. You need to import this object and pass your access token to it, as shown in the example below. ```python from qhub.quantum.sdk import HubQiskitProvider # set your access token token = "YOUR_ACCESS_TOKEN" provider = HubQiskitProvider(access_token=token) ``` ::: tip NOTE If your Qiskit code is executed in a service, the access token is automatically set by the platform. In this case the `access_token` parameter can be omitted. If it is set it is replaced by the service token. ::: After you have created the provider object you can list all backends supported by the platform and select the one you want to use, e.g., the `kipu.sim.qsim` backend: ```python # list all available quantum backends backends = qhub_provider.backends() # select certain backend backend = provider.get_backend("kipu.sim.qsim") ``` Now you can execute your Qiskit circuit on the selected backend, retrieve its `job` object, retrieve its results, cancel it etc. ```python from qiskit import QuantumCircuit, transpile # create a qiskit circuit circuit = QuantumCircuit(3, 3) circuit.h(0) circuit.cx(0, 1) circuit.cx(1, 2) circuit.measure(range(3), range(3)) # transpile circuit for backend circuit = transpile(circuit, backend) # execute circuit on selected backend job = backend.run(circuit, shots=1000) ``` ::: tip NOTE Executing your Qiskit code on the platform may lead to execution costs depending on selected backend and number of shots. Please find an overview about the costs for each backend on [our pricing page](https://kipu-quantum.com/platform/pricing/). ::: --- --- url: /services/orchestration/introduction.md description: >- Compose new services from existing ones using BPMN-based Service Orchestration powered by a Camunda workflow engine. --- # Introduction With Service Orchestration you have the ability build larger services from existing services. Service Orchestration means, that you build a new service by setting up a workflow where you call existing services in a defined order and where you can use the results of a service call as input for a following service call. A Service Orchestration therefore consists of a workflow (on BPMN base) which can be deployed and executed on a workflow engine (we use camunda). Usually you can set up such a workflow without the need of writing code. Only in case the data transfer between the service calls is not trivial, you may need to write some simple expressions or script statements. Once deployed, you can asynchronously execute your service and retrieve the results. ## Create a Service Orchestration You can create a Service Orchestration via the [create service page](https://dashboard.hub.kipu-quantum.com/services/new) of our UI. On the [create service page](https://dashboard.hub.kipu-quantum.com/services/new) of our UI, select Orchestration Service as service type and enter at least a meaningful name. The service will be created with a default workflow which consists only of a start node. You have to edit, deploy and publish the workflow before you can execute the service. ## Service Metadata The following table describes the metadata properties of a service. | Property | Description | |--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Name | Choose a meaningful name for your service. If you publish your service later on, this name will be displayed to other users. | | Service Type | Select "Service Orchestration". | | Description | Other users will see this description of the service, if its name sparked some interest, and they clicked on it in the marketplace. So any additional information you want to provide goes in here. | --- --- url: /public/files/openapi/README.md --- # KQH Service API Description This document describes the API for a KQH Service. It is used as a submodule in several projects directly. * [qhub-api](https://gitlab.com/kipu-all/kipuproduct-platform/qhub-api) * [qhub-commons](https://gitlab.com/kipu-all/kipuproduct-platform/qhub-commons) * [qhub-docs](https://gitlab.com/kipu-all/kipuproduct-platform/qhub-docs) * [qc-catalog](https://gitlab.com/kipu-all/kipuproduct-platform/qc-catalog) There are two versions available for [managed services](openapi-service.yaml) and [workflow services](openapi-service-workflow.yaml). --- --- url: /services/workflow/user-tasks.md description: >- Pause a workflow for a human decision or notify a user with a Platform User Task; the recipient responds from the in-app notification panel. --- # Notify and Ask Users Some workflows need a human in the loop. A **Platform User Task** pauses your workflow to ask a person a question — a yes/no decision or a free-text answer — or simply notifies them that something happened. The person is reached in the platform's in-app notification panel; when the task expects an answer, the workflow waits until they respond, then continues with their answer available as a workflow variable. ## When to use one * **Ask for a decision** before an expensive or irreversible step — for example, "Route this workload to IBM Miami?" before dispatching a paid backend run. * **Collect a short text answer** mid-workflow — an override value, a reason, a justification. * **Inform a user** that a milestone was reached, with no answer expected. ## Response types A Platform User Task has one of three response types. The response type decides both what the recipient sees and whether the workflow waits. | Response type | Recipient sees | Workflow behaviour | Captured answer | |---------------|-------------------------------|-------------------------------------------------|--------------------------| | `DECISION` | **Yes** / **No** buttons | Waits for the answer | Boolean (`true`/`false`) | | `FREE_TEXT` | A text box (up to 4000 chars) | Waits for the answer | String | | `NONE` | An informational message only | Does **not** wait — continues immediately | Nothing | `DECISION` and `FREE_TEXT` are **blocking**: the workflow parks at the task until someone answers. `NONE` is **fire-and-forget**: the message is delivered and the workflow moves on without pausing. ## Who receives it You do **not** choose the recipient. A Platform User Task always goes to the tenant that is running the workflow — the same account (user or organization) that started the execution. For an organization, every active member receives it and any authorised member can answer; the first answer completes the task. This is deliberate: the recipient is derived by the platform and cannot be redirected by the workflow, so a workflow can never send notifications or blocking tasks to an unrelated user or organization. ## Authoring in the workflow modeler A Platform User Task is a BPMN **User Task** carrying platform configuration. 1. From the palette, drag a **User Task** onto the canvas and connect it into your flow. 2. Select it and open the properties panel. A plain user task shows a **Configure as Platform User Task** call-to-action — the platform runtime ignores user tasks that are not configured. 3. Fill in the **Notification & response** group: | Field | What it is | |--------------------|-----------------------------------------------------------------------------------------------------------------------------| | **Message** | The text the recipient reads. A [FEEL expression](./data-manipulation.md) — see [Writing the message](#writing-the-message). | | **Response type** | `DECISION`, `FREE_TEXT`, or `NONE`. | | **Output variable**| The workflow variable that will hold the answer (blocking types only; disabled for `NONE`). | A freshly dropped task defaults to `DECISION` with a starter message and a pre-filled output variable, so it is runnable immediately — adjust the fields to your case. ![A Platform User Task "Confirm IBM Miami routing" selected in the workflow modeler; the Notification & response properties group shows Response type "Decision — true / false (blocking)", Output variable "routingApproved", and the FEEL Message "Route this workload to IBM Miami?"](./user-task-modeler.png) ::: info No recipient field There is no recipient setting. The task is always delivered to the tenant running the workflow (see [Who receives it](#who-receives-it)). ::: ### Writing the message The **Message** field is a FEEL expression evaluated against your workflow variables just before the notification is sent, so the recipient sees concrete values. ```javascript = "Route " + workloadName + " to IBM Miami?" ``` A constant message is simply a quoted string literal — bare unquoted text is not valid FEEL: ```javascript = "Please review and confirm before we continue." ``` ::: warning Plain text only The message is rendered as plain text. HTML or markup is rejected — keep the message to words, not tags. ::: ### Escalation is optional A blocking Platform User Task with no one to answer it will wait **indefinitely**. The modeler warns you when a blocking task has no escalation path but does not force one. To bound the wait, attach a **Timer Boundary Event** to the task and route it to a fallback path (a default value, an alternate branch, or a failure end event). `NONE` tasks never wait, so they need no escalation. ## What the recipient sees The task arrives in the recipient's in-app notification panel (the bell menu and the notifications page). * **`DECISION`** shows **Yes** and **No** buttons directly in the notification. * **`FREE_TEXT`** shows a text box with a **Submit** button (up to 4000 characters). * **`NONE`** shows the message as a plain informational entry the user reads and dismisses. ![An in-app notification titled "Response needed — IBM Routing Workflow" with the message "Route this workload to IBM Miami?", Yes and No buttons, and a "View workflow execution" link](./user-task-notification.png) For blocking tasks, a **View workflow** link is shown when the recipient is allowed to monitor the underlying execution, so they can open the running workflow for context before answering. Once anyone answers (or the task is cancelled or times out), the response controls disappear. ## Using the answer downstream For blocking tasks, the answer lands in the **output variable** you named and is available to every element after the task. The type follows the response type: `DECISION` yields a Boolean, `FREE_TEXT` yields a String. Branch on a `DECISION` answer with an **Exclusive Gateway** — set conditions on its outgoing sequence flows: ```javascript = routingApproved = true = routingApproved = false ``` Read a `FREE_TEXT` answer like any other workflow variable in a later task's input mapping or a gateway condition. ::: tip Pick a non-reserved output variable name The output variable follows the same naming rules as workflow output mappings. A handful of names are reserved by the runtime — see [Reserved workflow output variable names](./error-handling.md#reserved-workflow-output-variable-names). ::: ## Monitoring Platform User Tasks appear on the workflow **monitoring** view like any other activity. A task waiting for an answer is shown as **running**; its detail panel shows the status, timing, and — once answered — the captured response. A `NONE` task shows "Notification only (no response)". ## Related topics * [Common Workflow Compositions](./common-workflow-compositions.md) — where a human decision fits among fan-out, pipeline, and conditional patterns. * [Handling Errors in Workflow Services](./error-handling.md) — escalation paths and reserved output variable names. * [Data Manipulation](./data-manipulation.md) — FEEL expressions for the message and for reading the answer. --- --- url: /tutorials/tutorial-quera-mis.md description: >- Solve the Maximum Independent Set problem on QuEra Aquila via Analog Hamiltonian Simulation using the Quantum SDK Braket wrapper. --- # Solving the Maximum Independent Set Problem on QuEra Aquila using the Quantum SDK In this tutorial, you’ll learn how to use the Quantum SDK to perform Analog Hamiltonian Simulation on QuEra Aquila. The Quantum SDK serves as a wrapper around Braket, offering the same functionality and syntax. You can use the SDK either directly from your favorite IDE or within a [service](../services/managed/introduction). We will explore the Maximum Independent Set (MIS) problem as a practical example. This classic problem in graph theory involves finding the largest subset of nodes in a graph such that no two nodes in the subset are connected by an edge. The MIS problem has wide-ranging applications, including network design, scheduling, and resource allocation. In the example graph shown to the right, the MIS consists of Node 1 and Node 2. These nodes are not connected to each other, and adding any other node to this subset would create a connection between nodes within the subset, violating the independent set condition. This tutorial bases on the [AWS Braket QuEra tutorials](https://github.com/amazon-braket/amazon-braket-examples/blob/main/examples/analog_hamiltonian_simulation/00_Introduction_of_Analog_Hamiltonian_Simulation_with_Rydberg_Atoms.ipynb). We recommend reading it for more in-depth information about Analog Hamiltonian Simulation (AHS). ## Accessing Aquila with the Quantum SDK To access Aquila with platform SDK you need to have 1. A Kipu Quantum Hub Pro account. 2. The Quantum SDK. Running quantum programs on Aquila incurs execution costs, as detailed on the [pricing page](https://kipu-quantum.com/platform/pricing/). Therefore, you must have a **Pro** account with a valid credit card linked to it. If you haven't created an account yet, you can sign up [here](https://dashboard.hub.kipu-quantum.com/home). To upgrade to a Pro account, log in to your [Account settings](https://dashboard.hub.kipu-quantum.com/settings/account), click on `Upgrade`, and then select the `Subscribe` button under the `Pro` section. Follow the prompts to enter your credit card information. ::: tip NOTE If you are a member of an organization with a Pro account, you do not need to create an individual Pro account. ::: To install the Quantum SDK, ensure you have Python 3.11 or higher installed. The SDK is available on PyPI and can be installed using the following `pip` command: ```bash pip install qhub-quantum ``` ## Implementing the Maximum Independent Set problem with Aquila To solve an MIS problem using Aquila, one should first understand the nature of the problem and how it will be mapped onto Aquila. As stated above, the MIS problem on a given graph involves finding the largest subset of nodes that are not connected to each other. To encode the MIS problem mathematically, one must minimize a cost function that rewards a high number of nodes while penalizing the inclusion of nodes that are connected. On Aquila, this problem is natively encodable as you will see below. The qubit encoding in Aquila involves having atoms either in their fundamental ground state or being driven to a highly excited state, called the Rydberg state, through resonant excitations with a laser, referred to here as the driving laser. Entanglement arises when two atoms that are driven to make the transition are close enough to each other: the fact that the Rydberg state is highly excited in the electronic structure of the atom results in an interaction within a given radius, called the Blockade radius, with other atoms. If atoms are within this radius, driving the second atom to the excited state is not possible. This naturally creates the penalty condition of the cost function of the MIS. So if all atoms are driven to make the transition by controlling the parameters of the driving laser, but some are within the blockade radius of each other, this will create an independent set of atoms in the excited state. Then, using adiabatic computation to transition from a simple Hamiltonian to the Hamiltonian encoding the cost function of the MIS problem, one can solve the MIS problem using Aquila. ### Creating a program for Aquila to solve the MIS problem Let’s start by importing the relevant qhub and Braket libraries: ```python from qhub.quantum.sdk import HubBraketProvider from braket.ahs.atom_arrangement import AtomArrangement from braket.ahs.analog_hamiltonian_simulation import AnalogHamiltonianSimulation from braket.timings.time_series import TimeSeries from braket.ahs.driving_field import DrivingField ``` You can access the Aquila backend via its ID `aws.quera.aquila` using the `HubBraketProvider` object, as shown in the code snippet below. During initialization, you need to provide your access token and optionally your organization ID if you want to access the backend on behalf of your organization. The access token is required to authenticate your requests to the platform and to track the usage costs of your quantum executions. To obtain the token, visit your [Platform Home Page](https://dashboard.hub.kipu-quantum.com/home) and copy it from the `Your Personal Access Token` section. ```python provider = HubBraketProvider(access_token="your-access-token", organization_id="my-org-id") backend = provider.get_device("aws.quera.aquila") ``` ### Encoding the Graph using Rydberg atoms Each node in the graph will be represented by an individual Rydberg atom, and the edges will be encoded by positioning connected atoms within the Rydberg blockade radius of each other. The blockade radius is typically a few micrometers, and it determines the distance within which atoms cannot both be excited to Rydberg states simultaneously due to strong interactions. To systematically arrange the atoms, we’ll place them on a two-dimensional square grid with a spacing of 5.5 micrometers. This grid spacing ensures that atoms representing connected nodes are within each other’s blockade radius, effectively encoding the edges of the graph. * **Edge Encoding**: An edge between two nodes (atoms) is represented by placing them either directly adjacent (horizontally or vertically) or diagonally adjacent on the grid. * **Neighbor Distance**: * **Direct Neighbors**: For atoms that are horizontally or vertically adjacent, the distance is 5.5 micrometers. * **Diagonal Neighbors**: For atoms that are diagonally adjacent, the distance is \sqrt{2} \* 5.5 micrometers, calculated using the Pythagorean theorem. In Python the arrangement is implemented by the `atom_position` array. The order of nodes in this array corresponds to their indices in the result bitstring after measurement. ```python distance = 5.5e-6 # Distance in m atom_position = [ [ 0. , distance], # Node 0 [ 0. , 2*distance], # Node 1 [ distance, 0. ], # Node 2 [ distance, distance]] # Node 3 ``` Next, add the atom positions to the `AtomArrangement` object, which will serve as input for the AHS program that will be created later ```python register = AtomArrangement() for atom in atom_position: register.add(atom) ``` ### Driving the Analog Hamiltonian Simulation Aquila provides three control parameters for the laser that drives the electronic transition: the amplitude (Rabi frequency), the frequency/wavelength (detuning from the resonant frequency), and the phase of the laser relative to the atoms’ position. The initial and final values of the amplitude and detuning must be selected to represent a simple initial Hamiltonian and the final Hamiltonian encoding the MIS problem’s cost function. Typically, for adiabatic computation on Aquila, the initial values are: * `Amp(t=0) = 0` * `Detuning(t=0) < 0` And the values at time T, to represent the cost function of the MIS problem, are typically selected as follows:: * `Amp(t=T) = 0` * `Detuning(t=T) > 0` Generally, during the computation, the amplitude is increased to a certain value and then decreased before reaching its final value. To control these parameters on Aquila, we need to create a Braket object called TimeSeries, composed of a list of time points, each associated with values of the control parameters. Therefore, let’s first create the time points list, keeping in mind that the maximum duration for a computation on Aquila is 4 microseconds, and the minimum time step is 50 nanoseconds: ```python time_max = 4e-6 # seconds tsteps = 5e-8 # seconds nT=int(time_max/tsteps)+1 time=np.linspace(0, time_max, nT) ``` Now, let’s create the list of values for the control parameters. Here, we’ll use simple linear ramps without changing the phase, so we’ll create a list of zeros the same length as the time list: ```python # Create the control functions amplitude_min = 0 amplitude_max = 2.5e6 * 2 * np.pi # MHz*2*pi detuning_min = -9e6 * 2 * np.pi # MHz*2*pi detuning_max = 7e6 * 2 * np.pi # MHz*2*pi time_ramp = 0.15*time_max time_points = [0, time_ramp, time_max - time_ramp, time_max] amplitude_values = [amplitude_min, amplitude_max, amplitude_max, amplitude_min] detuning_values = [detuning_min, detuning_min, detuning_max, detuning_max] phase_values = [0, 0, 0, 0] amplitude_lin = np.interp(time, time_points, amplitude_values) detuning_lin = np.interp(time, time_points, detuning_values) phase_lin = np.interp(time, time_points, phase_values) ``` Next, let’s associate the list of values and the time points list to create TimeSeries objects: ```python # Creating the time series from the control functions amplitude = TimeSeries() for t_step, val in zip(time, amplitude_lin): amplitude.put(t_step, val) detuning = TimeSeries() for t_step, val in zip(time, detuning_lin): detuning.put(t_step, val) phase = TimeSeries() for t_step, val in zip(time, phase_lin): phase.put(t_step, val) ``` We can now create a DrivingField object, which corresponds to the driving part of Aquila’s Hamiltonian: ```python # Adding the time series to the driving field drive = DrivingField( amplitude=amplitude, detuning=detuning, phase=phase ) ``` We can then combine the atomic register created earlier with the DrivingField to create a complete Hamiltonian Simulation program. To ensure compatibility with the QuEra machine, we must round all values to match the precision levels supported by the Aquila QPU: ```python # Create the AHS program ahs_program = AnalogHamiltonianSimulation(register=register, hamiltonian=drive) # Discretize the AHS program according to the device's specifications discretized_program = ahs_program.discretize(backend) ``` The discretized program can now be run on Aquila via Kipu Quantum Hub by passing it to the backend’s `run` function, which returns a Braket task. You can monitor its execution state by calling the `state` function. After its successful execution, you can retrieve the results: ```python # Execute the discretized AHS program on the selected backend task = backend.run(discretized_program) # Monitor task status and get results print(f"Task status: {task.state()}") print(f"Task result: {task.result()}") ``` ## Result Interpretation To process and interpret the results, we need to explain how the measurement of qubit states is performed on Aquila: Before the time dependent sequence implemented in the AHS (pre-sequence), the atoms are prepared as described above, with all atoms in the fundamental ground state. The atoms at this stage are trapped by laser tweezers that individually keep the atoms in place. An imaging laser is then turned on using fluorescence to make a non-destructive detection of the atoms on the tweezers grid. We can verify if the sequence started with the right register. When the sequence starts, those tweezers are turned off, the computation is performed, and at the end, the trapping lasers are turned back on. The atoms that are still in the ground state are trapped, but the atoms that made the transition to the Rydberg state by the end of the computation are anti-trapped by the tweezers, meaning that because of their state, the tweezers will kick the atoms out of the register. The imaging laser is then turned on using fluorescence to detect the atoms, thus only detecting the atoms that stayed in the ground state. By processing the shot before and after, one can then deduce the state of the qubits: * Detected in the pre- and post-sequence shot: the atom is in the ground state (here labeled 0). * Detected in the pre- but not in the post-sequence: the atom is in the excited/Rydberg state (here labeled 1). In our example these are the two atoms representing the MIS nodes 1 and 2. * Not detected in the pre-sequence: defect in the register creation; this shot should be discarded. Using this understanding, we can process the measurement data to reconstruct, for each shot, the bitstring representing the qubits’ final states. The following Python code demonstrates how to build a dictionary of counts for each observed bitstring: ```python result = task.result() # Extract post-sequence measurements post_sequences = [list(measurement.post_sequence) for measurement in result.measurements] post_sequences = ["".join(['1' if site==0 else '0' for site in post_sequence]) for post_sequence in post_sequences] # Count the occurrences of each bitstring counters = {} for post_sequence in post_sequences: if post_sequence in counters: counters[post_sequence] += 1 else: counters[post_sequence] = 1 ``` The `counters` dictionary now contains the frequency of each observed bitstring, representing the different possible outcomes of the computation. For instance: * Based on our atom arrangement a bitstring like '0110', i.e. the solution to our problem, indicates that: * The second and the third atoms (MIS nodes 1 and 2) transitioned to the excited state ('1'). * The first and last two atoms (node 0 and node 3) remained in the ground state ('0'). ### Full code ```python from qhub.quantum.sdk import HubBraketProvider from braket.ahs.atom_arrangement import AtomArrangement from braket.ahs.analog_hamiltonian_simulation import AnalogHamiltonianSimulation from braket.timings.time_series import TimeSeries from braket.ahs.driving_field import DrivingField import numpy as np # Creates a simple task for Quera solving MIS for the graph given. # Instantiate the provider and select the QuEra Aquila backend provider = HubBraketProvider(access_token="your-access-token", organization_id="my-org-id") backend = provider.get_device(backend_id="aws.quera.aquila") # Define a simple atom arrangement distance = 5.5e-6 # Distance in m atom_position = [ [ 0. , distance], # Node 0 [ 0. , 2*distance], # Node 1 [ distance, 0. ], # Node 2 [ distance, distance]] # Node 3 # Add the atoms to the register register = AtomArrangement() for atom in atom_position: register.add(atom) # Create the time points list time_max = 4e-6 # seconds tsteps = 5e-8 # seconds nT=int(time_max/tsteps)+1 time=np.linspace(0, time_max, nT) # Create the control functions amplitude_min = 0 amplitude_max = 2.5e6 * 2 * np.pi # MHz*2*pi detuning_min = -9e6 * 2 * np.pi # MHz*2*pi detuning_max = 7e6 * 2 * np.pi # MHz*2*pi time_ramp = 0.15*time_max time_points = [0, time_ramp, time_max - time_ramp, time_max] amplitude_values = [amplitude_min, amplitude_max, amplitude_max, amplitude_min] detuning_values = [detuning_min, detuning_min, detuning_max, detuning_max] phase_values = [0, 0, 0, 0] amplitude_lin = np.interp(time, time_points, amplitude_values) detuning_lin = np.interp(time, time_points, detuning_values) phase_lin = np.interp(time, time_points, phase_values) # Creating the time series from the control functions amplitude = TimeSeries() for t_step, val in zip(time, amplitude_lin): amplitude.put(t_step, val) detuning = TimeSeries() for t_step, val in zip(time, detuning_lin): detuning.put(t_step, val) phase = TimeSeries() for t_step, val in zip(time, phase_lin): phase.put(t_step, val) # Adding the time series to the driving field drive = DrivingField( amplitude=amplitude, detuning=detuning, phase=phase ) # Create the AHS program ahs_program = AnalogHamiltonianSimulation(register=register, hamiltonian=drive) # Discretize the AHS program according to the device's specifications discretized_program = ahs_program.discretize(backend) # Execute the discretized AHS program on the selected backend task = backend.run(discretized_program) # Get results result = task.result() post_sequences = [list(measurement.post_sequence) for measurement in result.measurements] post_sequences = ["".join(['1' if site==0 else '0' for site in post_sequence]) for post_sequence in post_sequences] counters = {} for post_sequence in post_sequences: if post_sequence in counters: counters[post_sequence] += 1 else: counters[post_sequence] = 1 print(f"Result: {counters}") ``` --- --- url: /services/orchestration/workflow-editor.md description: >- Design BPMN workflows visually using service tasks, gateways, and data map objects to orchestrate subscribed platform services. --- # The Workflow Editor To open the workflow editor, go to the details page of your service and click on *Edit Service/Workflow* ## Start Editing When you start editing the first time, the workflow consists only of the start node: On the left side you find a panel which contains all possible node-types. Drag and drop a node into the editor area to start editing the workflow. Here are some examples of how a workflow may look like at the end: ## BPMN The workflows are based on the BPMN standard. For a more detailed description of how BPMN works, visit the related documentation on [Camunda](https://docs.camunda.io) e.g. [Camunda BPMN](https://docs.camunda.io/docs/components/modeler/bpmn). ### Nodes Here you find a brief description of the main node types, needed to build a useful platform workflow. | Node Type | Image | Description | |-----------------|-------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------| | Start | | The entry point for each workflow. | | End | | The final node. Here the execution ends. | | Service Task | | References a subscribed platfrom service. When executed, the service on the platform is called. | | Gateway | | Dependent on a condition the service flow can continue with different branches. | | Data Map Object | | Here you can define input/output data for you service invocations. | ### Add a Platform Service Node To call a platform service you have to add a service node and assign the service you want to call. Click on the change type icon and then on *Platform Service Tasks*. From the list of services, choose the one you want to call here. ::: warning NOTE Only services you are subscribed on can be used within a workflow. ::: #### Configure input data for the service node Assume the service node needs an input parameter with name "date" and type "string". You can either directly hard code this input value in the service node or configure a parameter name which then has to be given at call time. In both cases, click on the service node to open the content menu on the right side of the editor. Click on the plus icon of the Inputs row. If you want the parameter to be hard coded, just enter "date" as name and the wished date in the correct format, e.g. "2024-04-01" as value. If you want the parameter to be taken from the list of parameters you pass at service invocation, enter "data" as name and a reference to the parameter name where you pass the value later, e.g. ${myServiceData}. ### Add a Data Object to pass Input data to Node An alternative way to configure input and output data is to add data objects to the workflow, connect them with the service nodes, and configure the data within these data objects. An advantage of this is, that the data-flow is more explicit visible within the workflow. The behaviour is the same, it is also possible to mix data configuration at service node with data configuration via data objects. ::: warning NOTE If an input parameter is defined in both ways, at the service node and via a data object, the configuration via data object has the higher priority. ::: ::: warning NOTE If input data is hard coded in the workflow, it is not possible to overwrite the data at call invocation. So it is not possible to pass different data for different execution runs of the workflow. ::: --- --- url: /tutorials.md description: >- Index of hands-on tutorials covering Data Pools, Qiskit, QuEra Aquila, IBM Quantum, Qiskit Runtime, and local Service SDK development. --- # Tutorials * [Use DataPools in Manged Services](tutorial-datapool) * [Execute Qiskit Circuits using the Quantum SDK](tutorial-qiskit-with-platform-sdk) * [Solving the Maximum Independent Set Problem on QuEra Aquila using the Quantum SDK](tutorial-quera-mis) * [Utilize the Service SDK for Local Development](tutorial-local-development) * [Access IBM Quantum Platform Backends in a Service](tutorial-ibmq) * [Use Qiskit Runtime in a Service](tutorial-qiskit-runtime) --- --- url: /tutorials/tutorial-qiskit-runtime.md description: >- Build a service that uses qiskit-ibm-runtime sessions and samplers to run circuits on IBM Quantum Platform backends. --- # Use Qiskit Runtime in a Service This tutorial shows how to use the Qiskit Runtime SDK together with the IBM Quantum Platform from within a service. As an example, the code will generate some random numbers on the least busy IBM Quantum Platform backend. ## Bootstrap Project 1. Install the [CLI](../cli-reference). 2. Create a new project using `qhubctl init` and select the `Starter` template. 3. Open the project in your IDE of choice, e.g., VSCode. ## Create Python Environment Add the Qiskt Runtime SDK as a dependency to your project by adding `qiskit-ibm-runtime` to the `requirements.txt` file in the root folder. You can now set up a Python environment using Conda: ```bash conda env create -f environment.yml conda activate ``` Conda and the `environment.yml` file are used by the platform at runtime. However, if you do not have Conda installed on your local computer, you are also able to initialize a Python virtual environment using the tooling of your choice, e.g., `pyenv` or `venv`. You are now able to run the Python `src` folder as module from your console: ```bash python3 -m src ``` ## Extend Project Open the `program.py` in your IDE. The `run()` method is the main handler function and the entry point for your program. The method takes two arguments: (1) a `data` dictionary and (2) a `params` dictionary holding the input submitted by the user. The platform [translates](../services/managed/runtime-interface) the Service API body/payload in the form of `{ "data": { }, "params": { } }` into these parameters. It is also important that the `run()` method returns a JSON serializable `Response` object. The template makes use of the classes `ResultResponse` and `ErrorResponse`. It's recommended that you use these classes as well. Next, remove the whole code from within the `run()` method. Add some required import statements: ```python from typing import Dict, Any, Union, cast from qiskit import QuantumCircuit, transpile from qiskit_ibm_runtime import QiskitRuntimeService, Session, Sampler from qiskit_ibm_runtime.accounts import ChannelType ``` Add the following code to the `run()` method: ```python # defines the range of random numbers between 0 and 2^n_bits - 1 n_bits = data.get("n_bits", 2) channel: ChannelType = cast(ChannelType, os.getenv("QISKIT_IBM_CHANNEL", "ibm_quantum")) token: str = os.getenv("QISKIT_IBM_TOKEN", None) instance: str = os.getenv("QISKIT_IBM_INSTANCE", "ibm-q/open/main") service = QiskitRuntimeService(channel=channel, token=token, instance=instance) backend = service.least_busy(simulator=False, operational=True) circuit = QuantumCircuit(n_bits, n_bits) circuit.h(range(n_bits)) circuit.measure(range(n_bits), range(n_bits)) circuit = transpile(circuit, backend) start_time = time.time() with Session(service, backend=backend, max_time=None) as session: sampler = Sampler(session=session) job = sampler.run(circuit, shots=10) job_result = job.result() execution_time = time.time() - start_time session.close() random_number = int(list(job_result.quasi_dists[0].keys())[0]) ``` The code first instantiates the `QiskitRuntimeService` using required configuration coming from environment variables. You can use `QISKIT_IBM_CHANNEL` to define if you want to use the IBM Quantum Platform (`ibm_quantum`) or the IBM Cloud (`ibm_cloud`). With `QISKIT_IBM_TOKEN` you can specify your respective API token and with `QISKIT_IBM_INSTANCE` you can specify the instance string to be used when executing a circuit. The default values from the code above lets you run your program against the IBM Quantum Platform by just setting the `QISKIT_IBM_TOKEN` environment variables. Next, we determine the least busy backend followed by the creation of a simple circuit. By using a `Session`, we request a session to run our circuit in. Within this session, we can instantiate a `Sampler` and execute the problem by calling `sampler.run()`. Finally, we close the session once the result is present, which is when we can extract a random number out of the job result. Finally, return some result: ```python result = { "random_number": random_number, } metadata = { "execution_time": round(execution_time, 3), } return ResultResponse(result=result, metadata=metadata) ``` ::: details Source Code (program.py) The full project can be found in our [Implementations `python-starter`](https://dashboard.hub.kipu-quantum.com/community/implementations/1a0ae675-4b23-405c-af8e-f4189ff14e0f). ```python import os import time from typing import Dict, Any, Union, cast from qiskit import QuantumCircuit, transpile from qiskit_ibm_runtime import QiskitRuntimeService, Session, Sampler from qiskit_ibm_runtime.accounts import ChannelType from .libs.return_objects import ResultResponse, ErrorResponse def run(data: Dict[str, Any] = None, params: Dict[str, Any] = None) -> Union[ResultResponse, ErrorResponse]: # defines the range of random numbers between 0 and 2^n_bits - 1 n_bits = data.get("n_bits", 2) # initialize qiskit runtime service channel: ChannelType = cast(ChannelType, os.getenv("QISKIT_IBM_CHANNEL", "ibm_quantum")) token: str = os.getenv("QISKIT_IBM_TOKEN", None) instance: str = os.getenv("QISKIT_IBM_INSTANCE", "ibm-q/open/main") service = QiskitRuntimeService(channel=channel, token=token, instance=instance) backend = service.least_busy(simulator=False, operational=True) # create circuit circuit = QuantumCircuit(n_bits, n_bits) circuit.h(range(n_bits)) # perform measurement circuit.measure(range(n_bits), range(n_bits)) # transpile circuit circuit = transpile(circuit, backend) start_time = time.time() with Session(service, backend=backend, max_time=None) as session: sampler = Sampler(session=session) job = sampler.run(circuit, shots=10) job_result = job.result() execution_time = time.time() - start_time session.close() # extract random number random_number = int(list(job_result.quasi_dists[0].keys())[0]) result = { "random_number": random_number, } metadata = { "execution_time": round(execution_time, 3), } return ResultResponse(result=result, metadata=metadata) ``` ::: ## Run the Project Locally Run the program using `QISKIT_IBM_TOKEN=9356f0193daa... python3 -m src` (copy the API token value from your IBM Quantum Platform account settings). The output should be similar to the following: ```shell {"result": {"random_number": 8}, "metadata": {"execution_time": 1467.637}} ``` The project, or the `__main__.py` respectively, uses the `data.json` and the `params.json` as input for the `run()` when executed locally. You may experiment with different inputs of the `n_bits` input data parameter. The next section shows how to create and run a service using the code you just have written. ## Create a Service We use the CLI to create a new service in your personal account. Login with the CLI: ```shell qhubctl login -t ``` Create the service: ```shell qhubctl up ``` After a while, the console should print something similar like this: ``` Pushing Image (2/2)... Service created 🚀 ``` Congratulations. You have successfully created a service. Before you can execute the service, a few more steps are necessary: 1. In case you want to run the circuit with a backend offered by the IBM Quantum Platform, you just have to add your respective API token in the Provider Access Tokens [settings](https://dashboard.hub.kipu-quantum.com/settings) of your account. In case you want to use the IBM Cloud, you have to add your IBM Cloud credentials by specifying the Service CRN and your API token. Then, the platform is able to provide the following environment variables at runtime: `QISKIT_IBM_INSTANCE` (Service CRN value) and `QISKIT_IBM_CHANNEL` (constant value: ibm\_cloud). 2. On the [service overview page](https://dashboard.hub.kipu-quantum.com/services), open your service and go to the Runtime Configuration (`Edit Service > Runtime Configuration`). Activate the option `Add secrets to runtime environment`. This option lets the platform inject your API token to the execution runtime. The value is made available through the environment variable `QISKIT_IBM_TOKEN`. And in case of IBM Cloud, also `QISKIT_IBM_INSTANCE` and `QISKIT_IBM_CHANNEL` are available at runtime. In your code, you already instrumented the `QiskitRuntimeService` accordingly whenever this environment variable is present. ## Run your Service Using the CLI, you can quickly run a Service Job: ```shell qhubctl run ``` The `run` command uses the `data.json` and `params.json` file as input for the job. You may adjust the values accordingly. Alternatively, you could have created a Service Job through the platform UI. More information about Jobs and how to use them can be found in our [documentation](../services/managed/jobs). Furthermore, you could also *publish your service for internal use* and read on how to use the service utilizing Applications. Just follow the steps in the [Using a Service](../services/using-a-service) section in our documentation. --- --- url: /tutorials/tutorial-datapool.md description: >- Hands-on tutorial building, deploying, and consuming a text analysis service that processes documents stored in Kipu Quantum Hub Data Pools. --- # Using Data Pools in Managed Services In this hands-on tutorial, you'll build a complete text analysis service that processes documents using our Data Pools feature. You'll learn how to upload datasets, create a service that reads from Data Pools, test it locally, deploy it, and consume it via the SDK. ## What You'll Build By the end of this tutorial, you'll have created: * A text analysis service that counts words in documents stored in Data Pools * A working local development environment * A deployed service on the platform * A Python client that consumes your service The full code of this tutorial is available in the [Implementations `service-using-data-pools`](https://dashboard.hub.kipu-quantum.com/community/implementations/5fc547be-a902-47e8-9914-6feb06e88eb7). ## Prerequisites * Node.js 20+ installed on your system * Python 3.11+ installed * A platform account with a personal access token **Note:** Replace ``, ``, `` and other placeholder values with your actual credentials throughout this tutorial. ## Step 1: Set Up Your Development Environment ### 1.1 Install and Configure the CLI First, let's install the CLI and verify it's working. Please also run this, if you already have the CLI installed, to ensure you have the latest version: ```bash # Install the current CLI npm install -g @quantum-hub/qhubctl # Verify installation qhubctl --version ``` You should see a version number. If you get an error, ensure Node.js 20+ is installed. ### 1.2 Install uv Package Manager We'll use uv, a fast Python package manager, for managing our Python dependencies: ```bash # Install uv (if not already installed) # On macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh # On Windows: # powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" # Verify uv installation uv --version ``` ### 1.3 Authenticate with the platform Get your personal access token from the platform (Profile → Access Tokens) and authenticate: ```bash qhubctl login -t ``` You should see a success message confirming you're logged in. ### 1.4 Create Your Service Project Let's create a new service project for our text analyzer: ```bash qhubctl init --name text-analyzer cd text-analyzer ``` This creates a project structure with: * `src/program.py` - Your main service logic * `input/` - Local test data directory * `qhub.json` - Service configuration * Other configuration files ### 1.5 Set Up Python Environment Now initialize a Python environment within our service project: ```bash # Initialize a Python project with uv in the current directory uv sync -U # Activate the environment (optional, uv will handle this automatically) source .venv/bin/activate # On Windows: .venv\Scripts\activate.[ps1|bat] ``` ## Step 2: Prepare Sample Data ### 2.1 Create Sample Text Files Let's create some sample documents to analyze. We'll create them in two locations - one set for uploading to the Data Pool and another set for local testing: ```bash # Create directories for both upload and local testing mkdir -p input/documents ``` Create sample documents for uploading to Data Pool in `input/documents/`: Create `input/documents/document1.txt`: ```bash cat > input/documents/document1.txt << 'EOF' Quantum computing is a revolutionary technology that harnesses the principles of quantum mechanics. It promises to solve complex problems that are intractable for classical computers. Quantum algorithms like Shor's algorithm and Grover's algorithm demonstrate significant speedups. EOF ``` Create `input/documents/document2.txt`: ```bash cat > input/documents/document2.txt << 'EOF' Machine learning and artificial intelligence are transforming industries worldwide. Deep learning models can process vast amounts of data to identify patterns. Natural language processing enables computers to understand human language. EOF ``` Create `input/documents/summary.json` with metadata: ```bash cat > input/documents/summary.json << 'EOF' { "collection": "Sample Documents", "total_files": 2, "description": "Demo text files for analysis", "created": "2025-08-04" } EOF ``` ### 2.2 Upload Data to a Data Pool Now upload the files from `input/documents/` to a Data Pool: ```bash qhubctl datapool upload -f ./input/documents/document1.txt -f ./input/documents/document2.txt -f ./input/documents/summary.json ``` The CLI will prompt you to create a new Data Pool. Choose "Yes" and give it a name like `text-analysis-demo`. **Save the Data Pool ID** that's returned - you'll need it later. ## Step 3: Implement the Text Analysis Service The full code of the text analysis service is available in the [Implementations `text-analyzer`](https://dashboard.hub.kipu-quantum.com/community/implementations/2fbca033-e049-40be-b988-82b14f019bc6). ### 3.1 Update the Service Logic Replace the contents of `src/program.py` with our text analyzer: ```python from qhub.commons.datapool import DataPool from pydantic import BaseModel import json from typing import Dict, List class AnalysisRequest(BaseModel): files_to_analyze: List[str] min_word_length: int = 3 class AnalysisResult(BaseModel): total_files: int word_counts: Dict[str, int] total_words: int summary: str def run(data: AnalysisRequest, documents: DataPool) -> AnalysisResult: """Analyze text files from a Data Pool and return word statistics.""" word_counts = {} files_processed = 0 for filename in data.files_to_analyze: try: # Read the text file from Data Pool with documents.open(filename, 'r') as f: content = f.read() # Simple word counting words = content.lower().split() for word in words: # Clean word and filter by length clean_word = ''.join(char for char in word if char.isalnum()) if len(clean_word) >= data.min_word_length: word_counts[clean_word] = word_counts.get(clean_word, 0) + 1 files_processed += 1 except FileNotFoundError: print(f"Warning: File {filename} not found in Data Pool") continue total_words = sum(word_counts.values()) # Find most common words top_words = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)[:5] summary = f"Analyzed {files_processed} files. Top words: {dict(top_words)}" return AnalysisResult( total_files=files_processed, word_counts=word_counts, total_words=total_words, summary=summary ) ``` ### 3.2 Make Your Initial Commit to Track Your Changes \[Optional] To track your changes, initialize a Git repository and commit your code: ```bash git init git add . git commit -m "Initial commit: Implement text analysis service" ``` ## Step 4: Test Locally ### 4.1 Set Up Local Test Environment Create test input in `input/data.json`: ```bash cat > input/data.json << 'EOF' { "files_to_analyze": ["document1.txt", "document2.txt"], "min_word_length": 4 } EOF ``` ### 4.2 Update Local Test Runner Replace `src/__main__.py` to test with our Data Pool: ```python import json import os from qhub.commons.constants import OUTPUT_DIRECTORY_ENV from qhub.commons.datapool import DataPool from qhub.commons.json import any_to_json from qhub.commons.logging import init_logging from .program import AnalysisRequest, run init_logging() # Set up output directory for local testing directory = "./out" os.makedirs(directory, exist_ok=True) os.environ[OUTPUT_DIRECTORY_ENV] = directory # Load test data with open("./input/data.json") as file: data = AnalysisRequest.model_validate(json.load(file)) # Simulate DataPool injection using local directory result = run(data, documents=DataPool("./input/documents")) print("Analysis Results:") print(any_to_json(result)) ``` ### 4.3 Run Local Test Test your service locally: ```bash python -m src ``` You should see output showing the word analysis results from your sample documents. ## Step 5: Deploy Your Service ### 5.1 Generate OpenAPI Specification ```bash qhubctl openapi ``` ### 5.2 Deploy Your Service to the Platform You have two options for deployment: using the CLI or the web UI. #### 5.2.1 Deploy via CLI To deploy your service using the CLI, run: ```bash qhubctl up ``` #### 5.2.2 Deploy via Web UI Alternatively, you can deploy via the platform web interface. Therefore, you need to compress your service files into a ZIP archive: ```bash qhubctl compress ``` 1. Go to the platform web interface and navigate to services: 2. Click on `Create Service` 3. Select your ZIP file at `Source` > `File` 4. Configure the service: * Set service name: "Text Analyzer with Data Pools" * Add a Data Pool parameter named `documents` 5. Publish the service **Save your service ID** - you'll need it for the next steps. ## Step 6: Test Your Deployed Service ### 6.1 Create a Request Body Create a file called `service-request.json` with the Data Pool reference: ```bash cat > service-request.json << 'EOF' { "data": { "files_to_analyze": ["document1.txt", "document2.txt"], "min_word_length": 3 }, "documents": { "id": "", "ref": "DATAPOOL" } } EOF ``` Replace `` with the Data Pool ID from Step 2.2. ### 6.2. Test the Execution Using the UI Currently, the Jobs execution using Data Pools as input is not available. Therefore, you need to publish your service first and invoke it via an Application. Follow these steps: 1. Go to the services page in the platform app: and navigate to your service. 2. Click on `Publish Service` and `Publish internally`. 3. Go to the Applications page: and create a new Application (or reuse an existing one). 4. Navigate to the Application you want to use. 5. Click on `Subscribe Internally` and select your new service. 6. After subscribing, you can test your service by clicking on `Try it out`. 7. Open the `POST` element in the OpenAPI specification. 8. Click again on `Try it out` and paste the content of `service-request.json` into the request body. 9. Click the `Execute` button under the body to run the service. 10. Navigate to the Application again and click on the subscription of your service on `Activity Logs`. 11. Select the latest execution and click on `Show Logs`. 12. You should see the execution logs, including the analysis results similar to the local execution. ## Step 7: Build a Python Client The full code of the client is available in the [Implementations `text-analyzer-client`](https://dashboard.hub.kipu-quantum.com/community/implementations/0da165d7-0a22-4f64-8474-afcad5cfb27b?). ### 7.1 Set Up Client Environment Create a separate directory for your client: ```bash cd .. mkdir text-analyzer-client cd text-analyzer-client # Set up Python environment uv init && uv sync -U uv add qhub-service python-dotenv source .venv/bin/activate # On Windows: .venv\Scripts\activate.ps1 ``` ### 7.2 Configure Client Credentials Create a `.env` file with your application credentials (get these from your application's settings page): You can get the `ACCESS_KEY_ID`, `SECRET_ACCESS_KEY`, and `DATAPOOL_ID` from the application you created in the previous steps. The `SERVICE_ENDPOINT` can be found and copied from the subscription of your service inside the application details. ```bash cat > .env << 'EOF' SERVICE_ENDPOINT= ACCESS_KEY_ID= SECRET_ACCESS_KEY= DATAPOOL_ID= EOF ``` ### 7.3 Create the Client Script Create `analyze_client.py`: ```python import os from dotenv import load_dotenv from qhub.service.client import HubServiceClient from qhub.service.datapool import DataPoolReference # Load environment variables load_dotenv() # Initialize the client client = HubServiceClient( os.getenv("SERVICE_ENDPOINT"), os.getenv("ACCESS_KEY_ID"), os.getenv("SECRET_ACCESS_KEY") ) def analyze_documents(files_to_analyze, min_word_length=3): """Run text analysis on documents in the Data Pool.""" # Create Data Pool reference documents = DataPoolReference(id=os.getenv("DATAPOOL_ID")) # Prepare request request_body = { "data": { "files_to_analyze": files_to_analyze, "min_word_length": min_word_length }, "documents": documents } print("Starting analysis...") # Execute the service execution = client.run(request=request_body) print(f"Execution started with ID: {execution.id}") print("Waiting for completion...") # Wait for completion execution.wait_for_final_state(timeout=300) if execution.status == "SUCCEEDED": result = execution.result() print("\n=== Analysis Results ===") print(f"Status: {execution.status}") print(f"Files processed: {result.total_files}") print(f"Total words found: {result.total_words}") print(f"Summary: {result.summary}") # Show top 10 most common words word_counts = result.word_counts top_words = sorted(word_counts.items(), key=lambda x: x[1], reverse=True)[:10] print("\nTop 10 most common words:") for word, count in top_words: print(f" {word}: {count}") else: print(f"Execution failed with status: {execution.status}") logs = execution.logs() print("Error logs:") for log in logs[-5:]: # Show last 5 log entries print(f" {log}") if __name__ == "__main__": # Analyze our sample documents analyze_documents( files_to_analyze=["document1.txt", "document2.txt"], min_word_length=4 ) ``` ### 7.4 Run the Client ```bash python analyze_client.py ``` You should see the text analysis results from your deployed service! ## Step 8: Advanced Usage ### 8.1 Add More Documents Upload additional documents to your Data Pool: ```bash cd ../text-analyzer-service # Create a new document cat > input/documents/document3.txt << 'EOF' Cloud computing provides scalable infrastructure for modern applications. Microservices architecture enables independent deployment and scaling. Container orchestration platforms manage distributed systems efficiently. EOF # Upload to existing Data Pool qhubctl datapool upload -f ./input/documents/document3.txt --datapool-id ``` ### 8.2 Analyze New Documents Update your client to analyze the new document: ```python # In analyze_client.py, change the files list: analyze_documents( files_to_analyze=["document1.txt", "document2.txt", "document3.txt"], min_word_length=5 ) ``` ### 8.3 Monitor Execution Progress Add progress monitoring to your client: ```python def analyze_with_monitoring(files_to_analyze, min_word_length=3): """Run analysis with real-time status monitoring.""" documents_ref = DataPoolReference(id=os.getenv("DATAPOOL_ID")) request_body = { "data": { "files_to_analyze": files_to_analyze, "min_word_length": min_word_length }, "documents": documents_ref } execution = client.run(request=request_body) print(f"Started execution: {execution.id}") # Monitor progress while not execution.has_finished: print(f"Status: {execution.status}") import time time.sleep(2) # Check every 2 seconds print(f"Final status: {execution.status}") if execution.status == "SUCCEEDED": return execution.result() else: print("Execution failed") return None ``` Then update the main block to use this function: ```python if __name__ == "__main__": # In analyze_client.py, change the files list: resutl = analyze_with_monitoring( files_to_analyze=["document1.txt", "document2.txt", "document3.txt"], min_word_length=5 ) print(result) if result else print("No results returned.") ``` And run it again: ```bash python analyze_client.py ``` You should see real-time status updates as your service processes the documents. ## What You've Accomplished 🎉 **Congratulations!** You've successfully: 1. ✅ Set up the CLI and authenticated 2. ✅ Created sample data and uploaded it to a Data Pool 3. ✅ Built a text analysis service that reads from Data Pools 4. ✅ Tested your service locally with simulated Data Pools 5. ✅ Deployed your service to the platform 6. ✅ Created a Python client that consumes your service 7. ✅ Learned how to monitor executions and handle results ## Key Concepts Learned * **Data Pools**: Managed file collections that can be mounted into services * **Local Testing**: Simulating Data Pools with local directories * **Service Parameters**: How Data Pool parameters are injected into your service * **SDK Integration**: Using a `DataPoolReference` to use DataPools in services * **Error Handling**: Managing file not found errors and execution failures ## Next Steps * Try uploading larger datasets (remember the 500 MB per file limit) * Experiment with different analysis algorithms * Build services that write results back to output Data Pools * Explore the workflow orchestration features for multistep data processing ## References \[CLI] [CLI Reference | Docs](https://docs.hub.kipu-quantum.com/cli-reference.html) \[DataPool] [Using Data Pools in Services | Docs](https://docs.hub.kipu-quantum.com/services/managed/datapool.html) \[SDK] [Service SDK Reference | Docs](https://docs.hub.kipu-quantum.com/sdk-reference-service.html) --- --- url: /tutorials/tutorial-local-development.md description: >- Run services locally with qhubctl serve and interact with them via the Service SDK to iterate before deploying to Kipu Quantum Hub. --- # Utilize the Service SDK for Local Development This tutorial provides step-by-step guidance on how to create services, monitor their statuses, retrieve their results, and cancel their executions locally. To accomplish this objective, the tutorial utilizes the Service SDK and CLI. Prerequisites: Ensure that Docker is installed and running properly. For detailed documentation, please refer to the following link: [Docker Desktop](https://www.docker.com/products/docker-desktop) ## Deploying the Services locally with the CLI To install the CLI, you must install Node.js and the npm command line interface using either a [Node version manager](https://github.com/nvm-sh/nvm) or a [Node installer](https://nodejs.org/en/download). Then install the CLI globally using npm: ```bash npm install -g @quantum-hub/qhubctl ``` Once the installation is complete, start by navigating to the directory where your project, which includes the service, is located. ```bash cd my-project ``` Next, run the following command: ```bash qhubctl serve ``` Once the SERVICE is up and running, you can access its API under . For additional details regarding the `qhubctl serve` functionality, please refer to the documentation available [here](../cli-reference#qhubctl-serve). Alternatively, you can access the service from your Python code through the [Service SDK](../sdk-service).