# WorkflowBuilder Guide ## What is the WorkflowBuilder? A single LLM is built with [`create()`](https://docs.qualcomm.com/doc/80-87189-2/topic/qairt-gen-ai-api-gen-ai-builder-factory.html#qairt.gen_ai_api.gen_ai_builder_factory.GenAIBuilderFactory.create) and produces a `GenAIContainer`. The **WorkflowBuilder** is the next step up: it assembles *multiple* builders into a single deployable unit — a `WorkflowContainer` — and describes how those components connect at runtime. The primary use case today is **Large Multimodal Models (LMMs)** such as Qwen2.5-VL and Qwen3-VL, where a Vision Transformer (image encoder) and a causal LLM (text decoder) must cooperate. The WorkflowBuilder handles both components with a single `build()` call and bundles the results for on-device execution. MULTI-COMPONENT BUILD (WorkflowBuilder) ============================================================ VisionEncoderBuilderHTP ─┐ ├─► WorkflowBuilder.build() ─► WorkflowContainer GenAIBuilderHTP ─┘ │ ▼ WorkflowContainer.save() WorkflowContainer.load() │ ▼ container.get_executor(device) │ ▼ ImageT2TExecutor / T2TExecutor Copy to clipboard ### When to use the WorkflowBuilder Use `WorkflowBuilder` when: - You are building a vision-language model (LMM) with separate encoder and decoder artifacts. - Your model has any multi-component topology with directed data flow between components. Use [`create()`](https://docs.qualcomm.com/doc/80-87189-2/topic/qairt-gen-ai-api-gen-ai-builder-factory.html#qairt.gen_ai_api.gen_ai_builder_factory.GenAIBuilderFactory.create) directly when: - You are building a single-component text-only LLM. Note `WorkflowBuilder.from_builders()` accepts a single-entry `builders` dict with no `workflow_graph` argument as a convenience wrapper for the single-component case. This is equivalent to calling `GenAIBuilderFactory.create()` and produces a `T2TExecutor` when executed. ## Core Concepts ### WorkflowGraph A `WorkflowGraph` is a directed graph that describes the topology of a multi-component model. It is defined by an ordered tuple of nodes and an optional tuple of directed connections between them. from qairt.gen_ai_api.configs.workflow import WorkflowGraph, WorkflowNode, WorkflowNodeRole graph = WorkflowGraph( nodes=( WorkflowNode(name="vision", role=WorkflowNodeRole.IMAGE_ENCODER), WorkflowNode(name="text", role=WorkflowNodeRole.TEXT_GENERATOR, output_callback_type="textCallback"), ), connections=( ("vision", "text", "standard"), ), ) Copy to clipboard ### Graph Validation Rules `WorkflowGraph` is **frozen** (Pydantic `ConfigDict(frozen=True)`): field reassignment is not allowed after construction. Any graph change requires constructing a new `WorkflowGraph`. This ensures the validation below always runs against the final state of the data. Four rules are checked at construction time and raise `ValueError` immediately: | Rule | Error message | | --- | --- | | No duplicate node names | `"Duplicate node name(s) in workflow graph: ['name', ...]"` | | Each connection must be exactly a 3-tuple | `"Connection at index N must have exactly 3 elements [source, target, connection_type], got M: ..."` | | Connection endpoints must reference declared nodes | `"Connection at index N references node(s) not declared in the graph: ['name']. Declared nodes: [...]"` | | `connection_type` must be `"standard"` or `"wildcard"` | `"Connection at index N has invalid connection_type '...'. Must be one of: ['standard', 'wildcard']"` | ### WorkflowNode Each node in the graph represents one model component. The fields are: | Field | Required | Description | | --- | --- | --- | | `name` | Yes | Unique identifier for the node within this graph. Used as the key in `builders` and
`containers` dictionaries. | | `role` | Yes | Functional role of the component (see [Node Roles](https://docs.qualcomm.com/doc/80-87189-2/topic/workflow_builder.html#node-roles) below). | | `output_callback_type` | No | Genie callback type string for streaming output from this node. Typically
`"textCallback"` for the text decoder. | | `path` | Conditional | Path to model artifacts. Required when using `WorkflowBuilder.from_workflow()`;
unused by `WorkflowBuilder.from_builders()`. | | `tokenizer_path` | No | Explicit path to the tokenizer. Overrides auto-discovery from `path`. | | `config_path` | No | Explicit path to the model `config.json`. Overrides auto-discovery from `path`. | ### Node Roles The `WorkflowNodeRole` enum defines the functional role of each component in the graph. The role determines which executor is selected by `WorkflowContainer.get_executor()` and how the Genie pipeline wires the components together at runtime. | Role | Description | | --- | --- | | `TEXT_GENERATOR` | Causal language model that produces text output. Every workflow must have exactly
one `TEXT_GENERATOR` node. | | `IMAGE_ENCODER` | Vision Transformer that processes image input into feature embeddings fed to the
text decoder. Presence of this role triggers `ImageT2TExecutor`. | | `TEXT_ENCODER` | Encodes text into embeddings for injection into the text decoder. The builder
automatically injects a `TEXT_ENCODER` node when `embedding_config` is present. | ### Connections and Connection Types Connections are directed edges in the graph. Each connection is a three-tuple: connections=( (source_node_name, target_node_name, connection_type), ) Copy to clipboard Two connection types are supported: | Type | Behaviour | | --- | --- | | `"standard"` | Role-based connector mapping. Genie automatically selects the connector based on
the source and target node roles. Use this for all standard topologies (e.g. image
encoder → text generator). | | `"wildcard"` | Bypasses role-based connector routing. Required for Qwen3-VL and similar models
where the vision encoder output does not map to a fixed role-based connector. | ## Building a Workflow There are two factory methods. Choose based on whether you have already constructed the component builders. ### from\_builders Use `from_builders` when you have fully configured `GenAIBuilder` (or `VisionEncoderBuilderHTP`) instances and want to wire them into a workflow. from qairt.gen_ai_api.builders.workflow_builder import WorkflowBuilder workflow_builder = WorkflowBuilder.from_builders( builders={"vision": vision_builder, "text": text_builder}, workflow_graph=graph, ) container = workflow_builder.build() Copy to clipboard Parameters: - **builders** – `Dict[str, Buildable]`. Keys must match the `name` fields of the nodes in `workflow_graph`. - **workflow\_graph** – `Optional[WorkflowGraph]`. If omitted and `builders` has exactly one entry, a single-node `TEXT_GENERATOR` graph is created automatically. If omitted and `builders` has more than one entry, a `ValueError` is raised. Raises: - `ValueError` – if `workflow_graph` is `None` and `builders` has more than one entry. - `ValueError` – if `workflow_graph` is `None` and `builders` is empty. ### from\_workflow Use `from_workflow` when you have a `WorkflowGraph` whose nodes each carry a `path` and want the builder to construct the component builders automatically from those paths. from qairt.gen_ai_api.builders.workflow_builder import WorkflowBuilder from qairt.gen_ai_api.configs.workflow import WorkflowGraph, WorkflowNode, WorkflowNodeRole graph_with_paths = WorkflowGraph( nodes=( WorkflowNode( name="vision", role=WorkflowNodeRole.IMAGE_ENCODER, path="/path/to/vit.onnx", config_path="/path/to/config.json", ), WorkflowNode( name="text", role=WorkflowNodeRole.TEXT_GENERATOR, path="/path/to/llm.onnx", tokenizer_path="/path/to/tokenizer.json", ), ), connections=(("vision", "text", "standard"),), ) workflow_builder = WorkflowBuilder.from_workflow( workflow=graph_with_paths, backend_type=BackendType.HTP, cache_root="./cache", ) container = workflow_builder.build() Copy to clipboard Parameters: - **workflow** – `WorkflowGraph`. Every node must have a `path` set. - **backend\_type** – `BackendType`. Backend for all component builders. Defaults to `BackendType.HTP`. - **cache\_root** – `Optional[os.PathLike]`. Shared cache root applied to every component builder. Defaults to `None`. Raises: - `ValueError` – if any node is missing a `path`. ## WorkflowContainer Lifecycle ### Building Calling `build()` on the `WorkflowBuilder` invokes `build()` on each component builder and assembles the resulting containers into a `WorkflowContainer`: container = workflow_builder.build() # container is a WorkflowContainer Copy to clipboard Note If the built `TEXT_GENERATOR` container uses a split embedding table, the builder automatically inserts a `TEXT_ENCODER` node named `_encoder` (e.g. `"text_encoder"` for a node named `"text"`) into the returned `WorkflowContainer`. ### Saving and Loading `WorkflowContainer` serialises all sub-containers and the workflow graph to a single directory. Load the directory back to get a `WorkflowContainer` ready for execution without rebuilding. # Save after build container.save("./my_workflow_container", exist_ok=True) # Load in a later session (no rebuild required) from qairt.gen_ai_api.containers.workflow_container import WorkflowContainer container = WorkflowContainer.load("./my_workflow_container") Copy to clipboard `save()` parameters: - **dest** – destination directory path. - **exist\_ok** – if `False` (default), raises an error if the directory already exists. `load()` raises: - `NotADirectoryError` – if `path` is not a directory. - `FileNotFoundError` – if `metadata.json` is missing from the directory. ### Getting an Executor Call `get_executor()` on a loaded (or freshly built) `WorkflowContainer` to obtain an executor suitable for on-device inference. The method inspects the workflow topology and returns the appropriate executor type automatically: | Workflow topology | Executor returned | | --- | --- | | `TEXT_GENERATOR` only, or `TEXT_ENCODER` → `TEXT_GENERATOR` | `T2TExecutor` | | Any topology containing `IMAGE_ENCODER` | `ImageT2TExecutor` | executor = container.get_executor( device, engine_config=None, # optional EngineConfig clean_up=True, # delete on-device artifacts after execution ) Copy to clipboard Parameters: - **device** – optional `Device` instance. When omitted, the SDK picks the connected device. - **engine\_config** – optional `EngineConfig` for runtime engine settings. - **\*\*kwargs** – forwarded to the concrete executor constructor (e.g. `qairt_sdk_root`, `clean_up`). Raises: - `ValueError` – if the workflow contains no nodes. - `NotImplementedError` – if no `TEXT_GENERATOR` node is found. - `NotImplementedError` – if the topology is not yet supported. ### Running Inference After obtaining an executor, call `generate()` with a `GenerationRequest`: from qairt.gen_ai_api.configs.generation_config import GenerationRequest # Text-only result = executor.generate(GenerationRequest(messages="Hello!")) # Multimodal (image + text) — ImageT2TExecutor only result = executor.generate( GenerationRequest( messages=[ { "role": "user", "content": [ {"type": "image", "image": "/path/to/image.raw"}, {"type": "text", "text": "Describe this image."}, ], } ] ) ) result.print() Copy to clipboard For `ImageT2TExecutor`, image paths must point to pre-processed `.raw` files in the format expected by the model’s vision encoder. See [Vision-Language Model Inference on HTP](https://docs.qualcomm.com/doc/80-87189-2/topic/lmm_builder.html#lmm-builder) for the image preprocessing steps. After inference is complete, release on-device resources: executor.clean_environment() Copy to clipboard ## Workflow Patterns ### Vision-Language Model (standard connection) The standard topology for Qwen2.5-VL: an image encoder feeds into the text generator over a `"standard"` connection. graph = WorkflowGraph( nodes=( WorkflowNode(name="vision", role=WorkflowNodeRole.IMAGE_ENCODER), WorkflowNode(name="text", role=WorkflowNodeRole.TEXT_GENERATOR, output_callback_type="textCallback"), ), connections=(("vision", "text", "standard"),), ) container = WorkflowBuilder.from_builders( builders={"vision": vision_builder, "text": text_builder}, workflow_graph=graph, ).build() Copy to clipboard ### Vision-Language Model (wildcard connection) Some models (e.g. Qwen3-VL) require a `"wildcard"` connection because the vision encoder output does not map to a fixed role-based connector. The factory sets this automatically when it detects the architecture; if you are constructing the graph manually, use: graph = WorkflowGraph( nodes=( WorkflowNode(name="vision", role=WorkflowNodeRole.IMAGE_ENCODER), WorkflowNode(name="text", role=WorkflowNodeRole.TEXT_GENERATOR, output_callback_type="textCallback"), ), connections=(("vision", "text", "wildcard"),), ) Copy to clipboard ### Single-Component Text Generator For completeness, `WorkflowBuilder` also supports wrapping a single text-only builder. Omit the `workflow_graph` argument and a `TEXT_GENERATOR` graph is created automatically: container = WorkflowBuilder.from_builders({"text": genai_builder}).build() executor = container.get_executor(device) # returns T2TExecutor Copy to clipboard For a complete end-to-end example that builds a vision-language model from ONNX artifacts, compiles for a target chipset, and runs multimodal inference on device, see [Vision-Language Model Inference on HTP](https://docs.qualcomm.com/doc/80-87189-2/topic/lmm_builder.html#lmm-builder). ## API Reference The full auto-generated API documentation for all workflow classes is in the API section: - [WorkflowBuilder, VisionEncoderBuilderHTP](https://docs.qualcomm.com/doc/80-87189-2/topic/qairt-gen-ai-api-builders.html) - [WorkflowContainer](https://docs.qualcomm.com/doc/80-87189-2/topic/qairt-gen-ai-api-containers.html) - [WorkflowGraph, WorkflowNode, WorkflowNodeRole](https://docs.qualcomm.com/doc/80-87189-2/topic/qairt-gen-ai-api-configs-workflow.html) Last Published: Aug 26, 2026 [Previous Topic Backend Extension JSON Files](https://docs.qualcomm.com/bundle/publicresource/80-87189-2/topics/genai_migration.md) [Next Topic ONNX Optimizer](https://docs.qualcomm.com/bundle/publicresource/80-87189-2/topics/guides.md)