Transforms
Documents the transform implementations. To view artifacts as an operator, see Viewing Artifacts.
Overview
Transforms convert Infrahub data into usable outputs (configs, documentation, computed attributes). They run on-demand when artifacts are accessed.
Transform types
- Python Transforms - Complex logic, external library calls (PyAVD)
- Jinja2 Transforms - Template-based text generation
Every transform this repository registers is a Python transform. One of them,
containerlab_topology, renders its output through a Jinja2 template it loads itself; the
jinja2_transforms: block of .infrahub.yml is unused here.
Transform architecture
┌──────────────────┐ ┌────────────────────┐ ┌─────────────────┐
│ GraphQL Query │ ──▶ │ Transform Class │ ──▶ │ Artifact │
│ (*.gql) │ │ (*Transform) │ │ (config/doc) │
└──────────────────┘ └────────────────────┘ └─────────────────┘
Python transforms
ComputedInterfaceDescription
File: transforms/computed_interface_description.py
Purpose: Generate human-readable interface descriptions
Input: DcimInterface Output: String like "→ remote-device:Ethernet1"
class ComputedInterfaceDescription(InfrahubTransform):
async def transform(self, data):
interface = data["DcimInterface"]["edges"][0]["node"]
link = interface.get("link")
if not link:
return ""
# Find the remote end
remote = link["interface_a"] if link["interface_b"]["id"] == interface["id"] else link["interface_b"]
return f"→ {remote['device']['name']['value']}:{remote['name']['value']}"
CablingPlan
File: transforms/cabling_plan.py
Purpose: Generate CSV cabling documentation for a fabric
Input: NetworkFabric Output: CSV file with all connections
Source Device,Source Interface,Destination Device,Destination Interface,Link Type
spine-A1-1,Ethernet1,super-spine-A-1,Ethernet1,uplink
leaf-A1-01-1,Ethernet49,spine-A1-1,Ethernet1,uplink
AvdEosConfigTransform
File: transforms/avd_eos_config.py
Purpose: Convert AVD structured config to EOS CLI
Input: DcimDevice (with AvdArtifact) Output: EOS CLI configuration
class AvdEosConfigTransform(InfrahubTransform):
async def transform(self, data):
device = data["DcimDevice"]["edges"][0]["node"]
artifact = device["avd_artifact"]["node"]
if not artifact["structured_config_identifier"]["value"]:
return "! No structured config available"
config = await self.client.object_store.get(
identifier=artifact["structured_config_identifier"]["value"]
)
return pyavd.get_device_config(json.loads(config))
AvdFabricDocTransform
File: transforms/avd_fabric_doc.py
Purpose: Generate fabric-wide documentation
Input: NetworkFabric Output: Markdown documentation
class AvdFabricDocTransform(InfrahubTransform):
async def transform(self, data):
# Collect all device hostvars and structured configs
all_hostvars = {}
all_structured = {}
for device in devices:
hostvars = await self.client.object_store.get(...)
structured = await self.client.object_store.get(...)
all_hostvars[device.name] = hostvars
all_structured[device.name] = structured
avd_facts = pyavd.get_avd_facts(all_hostvars)
return pyavd.get_fabric_documentation(
avd_facts, all_structured, fabric_name
)
AvdDeviceDocTransform
File: transforms/avd_device_doc.py
Purpose: Generate per-device documentation
Input: DcimDevice Output: Markdown documentation for single device
AvdAntaCatalogTransform
File: transforms/avd_anta_catalog.py
Purpose: Render a per-device ANTA test catalog from the stored structured config
Input: DcimDevice Output: YAML catalog, or a one-line marker comment
Unlike EOS config rendering, catalog generation needs fabric-wide data, so the transform gathers
every sibling device's structured config in the same fabric into one AVDFabricData before calling
pyavd.get_device_test_catalog().
It is gated by the fabric's anta_enabled flag. When ANTA is disabled — or the device has no
fabric, or no structured config — the transform returns a marker comment instead of a catalog, so
the artifact renders successfully and says why it is empty:
# ANTA disabled for fabric Fabric-L3LS-Multi-Domain
ContainerLabTopology
File: transforms/containerlab_topology.py
Purpose: Render a ContainerLab topology file for a whole fabric
Input: NetworkFabric
Output: YAML topology (topology.clab.yml shape)
The transform uses two queries — containerlab_topology for the fabric's devices and
containerlab_link_endpoints to resolve link endpoints in batches — and renders through the
transforms/templates/containerlab_topology.j2 template. Node kinds, container images, and the
interface-mapping bind come from schema attributes (DcimPlatform.containerlab_os,
DcimPlatform.containerlab_image, DcimDeviceType.containerlab_interface_mapping), so changing the
cEOS version is a data change rather than a code change.
Nodes and links are emitted in a stable sorted order, so two renders of unchanged data are byte-identical. See the ContainerLab page for the full artifact shape, the role-selection rules, and how to deploy the topology.
CVWorkspaceSubmissionWebhookPayload
File: transforms/cv_workspace_submission_webhook.py
Purpose: Build the JSON body for the CloudVision workspace-submission CoreCustomWebhook
Input: CloudvisionWorkspace Output: JSON object — check name, proposed-change ID, and one entry per linked workspace with its ID, status, URL, and fabric name
This transform has no artifact definition: it is referenced by the webhook rather than rendered to the object store. See Checks for how it fits the CloudVision validation pipeline.
Query classes
Each transform has Pydantic models for type-safe query parsing. These *_query.py files are generated, not hand-written — regenerate them whenever the .gql query or the schema changes:
uv run infrahubctl graphql generate-return-types transforms/computed_interface_description.gql
This reads schema.graphql at the repo root (refresh with uv run infrahubctl graphql export-schema --destination schema.graphql when needed) and emits the matching *_query.py next to the query file.
Shape of a typical generated class:
# transforms/computed_interface_description_query.py (generated)
class InterfaceLink(BaseModel):
interface_a: InterfaceNode
interface_b: InterfaceNode
class InterfaceNode(BaseModel):
id: str
name: ValueWrapper[str]
device: DeviceRef
link: InterfaceLink | None
Artifacts
Artifacts are the output files generated by transforms.
Artifact definitions
Defined in .infrahub.yml:
artifact_definitions:
- name: cabling_plan
targets: fabrics
transformation: cabling_plan
content_type: text/csv
- name: avd_eos_configuration
targets: avd_devices
transformation: avd_eos_config
content_type: text/plain
- name: avd_fabric_documentation
targets: fabrics
transformation: avd_fabric_doc
content_type: text/markdown
- name: avd_device_documentation
targets: avd_devices
transformation: avd_device_doc
content_type: text/markdown
- name: avd_anta_catalog
targets: avd_devices
transformation: avd_anta_catalog
content_type: application/yaml
- name: containerlab_topology
targets: fabrics
transformation: containerlab_topology
content_type: application/yaml
cv_workspace_submission_webhook_payload is deliberately absent from this block — it renders a
webhook body, not an artifact.
Viewing artifacts
- Navigate to target object in Infrahub UI
- Click Artifacts tab
- Select artifact to view/download
Regenerating artifacts
Artifacts regenerate automatically when:
- Underlying data changes
- Transform code changes
- Manually triggered via UI
Configuration
Transforms are registered in .infrahub.yml:
python_transforms:
- name: computed_interface_description
class_name: ComputedInterfaceDescription
file_path: "./transforms/computed_interface_description.py"
- name: cabling_plan
class_name: CablingPlan
file_path: "./transforms/cabling_plan.py"
- name: avd_eos_config
class_name: AvdEosConfigTransform
file_path: "./transforms/avd_eos_config.py"
- name: avd_fabric_doc
class_name: AvdFabricDocTransform
file_path: "./transforms/avd_fabric_doc.py"
- name: avd_device_doc
class_name: AvdDeviceDocTransform
file_path: "./transforms/avd_device_doc.py"
- name: avd_anta_catalog
class_name: AvdAntaCatalogTransform
file_path: "./transforms/avd_anta_catalog.py"
- name: containerlab_topology
class_name: ContainerLabTopology
file_path: "./transforms/containerlab_topology.py"
- name: cv_workspace_submission_webhook_payload
class_name: CVWorkspaceSubmissionWebhookPayload
file_path: "./transforms/cv_workspace_submission_webhook.py"
convert_query_response: false
File structure
transforms/
├── computed_interface_description.py # Interface description
├── computed_interface_description.gql # Interface query
├── computed_interface_description_query.py # Pydantic models
├── cabling_plan.py # Cabling plan CSV
├── fabric_cabling_plan.gql # Fabric cabling query
├── fabric_cabling_plan_query.py # Pydantic models
├── avd_eos_config.py # EOS config transform
├── avd_device_config.gql # Device config query
├── avd_device_config_query.py # Pydantic models
├── avd_fabric_doc.py # Fabric documentation
├── avd_device_doc.py # Device documentation
├── avd_fabric_devices.gql # Fabric devices query
├── avd_fabric_devices_query.py # Pydantic models
├── avd_anta_catalog.py # ANTA catalog transform
├── avd_anta_catalog.gql # ANTA catalog query
├── avd_anta_catalog_query.py # Pydantic models
├── containerlab_topology.py # ContainerLab topology transform
├── containerlab_topology.gql # Fabric device/link query
├── containerlab_topology_query.py # Pydantic models
├── containerlab_link_endpoints.gql # Batched link-endpoint query
├── containerlab_link_endpoints_query.py # Pydantic models
├── cv_workspace_submission_webhook.py # CloudVision webhook payload
├── cv_workspace_submission_webhook.gql # Workspace query
├── cv_workspace_submission_webhook_query.py # Pydantic models
└── templates/
└── containerlab_topology.j2 # ContainerLab topology template
Creating new transforms
Python transform
- Create transform class:
# transforms/my_transform.py
from infrahub_sdk.transforms import InfrahubTransform
class MyTransform(InfrahubTransform):
query = "my_query"
async def transform(self, data):
# Process data
return "output"
- Create GraphQL query:
# transforms/my_query.gql
query MyQuery($device_id: String!) {
DcimDevice(ids: [$device_id]) {
edges {
node {
name { value }
}
}
}
}
- Register in
.infrahub.yml:
queries:
- name: my_query
file_path: "./transforms/my_query.gql"
python_transforms:
- name: my_transform
class_name: MyTransform
file_path: "./transforms/my_transform.py"
artifact_definitions:
- name: my_artifact
targets: devices
transformation: my_transform
Jinja2 transform
- Create template:
{# transforms/templates/my_template.j2 #}
Output for {{ node.name.value }}
- Register in
.infrahub.yml:
jinja2_transforms:
- name: my_jinja_transform
template_path: "./transforms/templates/my_template.j2"
query: my_query
Source
- Python transforms:
transforms/avd_eos_config.py—AvdEosConfigTransform.transforms/avd_fabric_doc.py—AvdFabricDocTransform.transforms/avd_device_doc.py—AvdDeviceDocTransform.transforms/computed_interface_description.py—ComputedInterfaceDescription.transforms/cabling_plan.py—CablingPlan.transforms/avd_anta_catalog.py—AvdAntaCatalogTransform.transforms/containerlab_topology.py—ContainerLabTopology.transforms/cv_workspace_submission_webhook.py—CVWorkspaceSubmissionWebhookPayload.
- Templates:
transforms/templates/. - Registration:
.infrahub.yml—python_transforms:andartifact_definitions:blocks. - The AVD transforms are documented in detail on the AVD Transforms page.
- The CloudVision webhook payload transform is documented alongside the Checks it serves.