Skip to main content
Jinja2 templates provide a powerful way to generate text-based artifacts from Infrahub data. They’re ideal for creating device configurations, documentation, and structured text output.

What is Jinja2?

Jinja2 is a modern templating engine for Python that allows you to generate text output with:
  • Variable substitution
  • Control flow (loops, conditionals)
  • Filters and functions
  • Template inheritance
  • Macros for reusable logic
Infrahub uses Jinja2 to transform GraphQL query results into configuration files and other text-based artifacts.

Creating a Jinja2 Transform

1. Define a GraphQL Query

First, create a GraphQL query to retrieve the data you need:
# templates/device_startup_info.gql
query device_startup_info ($device: String!) {
  InfraDevice(name__value: $device) {
    edges {
      node {
        id
        name { value }
        asn {
          node {
            asn { value }
          }
        }
        interfaces {
          edges {
            node {
              id
              name { value }
              description { value }
              enabled { value }
              mtu { value }
              role { value }
              ... on InfraInterfaceL3 {
                ip_addresses {
                  edges {
                    node {
                      address { value }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

2. Create the Jinja2 Template

Create a template file that uses the GraphQL query data:
{# templates/device_startup_config.tpl.j2 #}
hostname {{ data.InfraDevice.edges[0].node.name.value }}
!
username admin privilege 15 secret admin123
!
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
interface {{ intf.node.name.value }}
{% if intf.node.description.value %}
  description {{ intf.node.description.value }}
{% else %}
  description role: {{ intf.node.role.value }}
{% endif %}
{% if intf.node.mtu.value %}
  mtu {{ intf.node.mtu.value }}
{% endif %}
{% if not intf.node.enabled.value %}
  shutdown
{% endif %}
{% if intf.node.ip_addresses %}
{% for ip in intf.node.ip_addresses.edges %}
  ip address {{ ip.node.address.value }}
  no switchport
{% endfor %}
{% endif %}
!
{% endfor %}

3. Register in .infrahub.yml

Add the query and transform to your repository configuration:
# .infrahub.yml
queries:
  - name: device_startup_info
    file_path: "templates/device_startup_info.gql"

jinja2_transforms:
  - name: device_startup
    description: "Template to generate startup configuration for network devices"
    query: device_startup_info
    template_path: "templates/device_startup_config.tpl.j2"

Data Structure

The GraphQL query response is passed to the template as a data dictionary with the exact structure returned by GraphQL:
data = {
    "InfraDevice": {
        "edges": [
            {
                "node": {
                    "id": "...",
                    "name": {"value": "edge-router-01"},
                    "interfaces": {
                        "edges": [
                            {
                                "node": {
                                    "name": {"value": "GigabitEthernet0/0"},
                                    "enabled": {"value": true},
                                    "role": {"value": "uplink"}
                                }
                            }
                        ]
                    }
                }
            }
        ]
    }
}
Access data in templates using dot notation:
{{ data.InfraDevice.edges[0].node.name.value }}

Template Syntax Reference

Variables

Access and output variables:
{{ data.InfraDevice.edges[0].node.name.value }}
{{ intf.node.description.value }}
{{ ip.node.address.value }}

Conditionals

Control flow with if/elif/else:
{% if intf.node.description.value %}
  description {{ intf.node.description.value }}
{% else %}
  description role: {{ intf.node.role.value }}
{% endif %}

{% if intf.node.role.value == "loopback" %}
  ip ospf network loopback
{% elif intf.node.role.value == "peer" %}
  ip ospf network point-to-point
{% endif %}

Loops

Iterate over lists:
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
interface {{ intf.node.name.value }}
!
{% endfor %}

{% for ip in intf.node.ip_addresses.edges %}
  ip address {{ ip.node.address.value }}
{% endfor %}

Namespace Variables

Use namespace to share variables across scopes:
{% set ns = namespace(loopback_ip=none, management_ip=none) %}
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
{%   if intf.node.role.value == "loopback" %}
{%     set ns.loopback_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
{%   elif intf.node.role.value == "management" %}
{%     set ns.management_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
{%   endif %}
{% endfor %}

router ospf 1
  router-id {{ ns.loopback_ip }}

Filters

Modify variables with filters:
{# String operations #}
{{ device_name | upper }}
{{ description | lower }}
{{ text | replace('old', 'new') }}

{# String manipulation #}
{{ ip_address | split('/')[0] }}

{# List operations #}
{{ interfaces | length }}
{{ interfaces | first }}
{{ interfaces | last }}

Comments

Add comments that won’t appear in output:
{# This is a comment #}
{# 
  Multi-line comment
  Won't appear in output
#}

Real-World Example: Arista Device Configuration

Complete example from Infrahub’s demo repository:
{% set ns = namespace(loopback_intf_name=none, loopback_ip=none, management_intf_name=none, management_ip=none) %}
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
{%   if intf.node.role.value == "loopback" %}
{%     set ns.loopback_intf_name = intf.node.name.value %}
{%     set ns.loopback_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
{%   elif intf.node.role.value == "management" %}
{%     set ns.management_intf_name = intf.node.name.value %}
{%     set ns.management_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
{%   endif %}
{% endfor %}
no aaa root
!
username admin privilege 15 role network-admin secret sha512 $6$...
!
transceiver qsfp default-mode 4x10G
!
service routing protocols model multi-agent
!
hostname {{ data.InfraDevice.edges[0].node.name.value }}
!
spanning-tree mode mstp
!
management api http-commands
   no shutdown
!
management api gnmi
   transport grpc default
!
management api netconf
   transport ssh default
!
{% for intf in data.InfraDevice.edges[0].node.interfaces.edges %}
{%   if intf.node.name.value != ns.management_intf_name and intf.node.name.value != ns.loopback_intf_name %}
interface {{ intf.node.name.value }}
{%   if intf.node["description"]["value"] %}
   description {{ intf.node["description"]["value"] }}
{%   else %}
   description role: {{ intf.node.role.value }}
{%   endif %}
{%   if "mtu" in intf.node and intf.node["mtu"]["value"] %}
   mtu {{ intf.node["mtu"]["value"] }}
{%   endif %}
{%   if not intf.node["enabled"]["value"] %}
   shutdown
{%   endif %}
{%   if intf.node["ip_addresses"] %}
{%     for ip in intf.node["ip_addresses"]["edges"] %}
   ip address {{ ip.node["address"]["value"] }}
   no switchport
{%       if intf.node.role.value == "peer" or intf.node.role.value == "backbone" %}
   ip ospf network point-to-point
{%       endif %}
{%     endfor %}
{%   endif %}
!
{%   endif %}
{% endfor %}
!
interface {{ ns.management_intf_name }}
{% for intf in data.InfraDevice.edges[0]["interfaces"] %}
{%   if intf.node.name.value == ns.management_intf_name %}
{%     for ip in intf["ip_addresses"] %}
   ip address {{ ip["address"]["value"] }}
{%     endfor %}
{%   endif %}
{% endfor %}
!
interface {{ ns.loopback_intf_name }}
{% for intf in data.InfraDevice.edges[0]["interfaces"] %}
{%   if intf.node.name.value == ns.loopback_intf_name %}
{%     for ip in intf["ip_addresses"] %}
   ip address {{ ip["address"]["value"] }}
{%     endfor %}
{%   endif %}
{% endfor %}
!
ip routing
!
{% if data.InfraDevice.edges[0].node.asn %}
router bgp {{ data.InfraDevice.edges[0].node.asn.node.asn.value }}
   router-id {{ ns.loopback_ip }}
!
{% endif %}
!
router ospf 1
   router-id {{ ns.loopback_ip }}
   redistribute connected
   max-lsa 12000
   passive-interface Loopback0
   network 0.0.0.0/0 area 0.0.0.0
!
end

Simple Example: Data Transformation

For simpler use cases like data extraction:
{# templates/person_with_cars.j2 #}
Name: {{ data.TestingPerson.edges[0].node.name.value }}
With configuration:
jinja2_transforms:
  - name: person_with_cars
    description: "Template to a report card showing a person and the cars they own"
    query: "person_with_cars"
    template_path: "templates/person_with_cars.j2"

Using with Artifacts

Combine Jinja2 transforms with artifact definitions:
artifact_definitions:
  - name: "Startup Config for Edge devices"
    artifact_name: "startup-config"
    parameters:
      device: "name__value"
    content_type: "text/plain"
    targets: "edge_router"
    transformation: "device_startup"  # References jinja2_transforms name
When the artifact is generated:
  1. Target devices are identified from the edge_router group
  2. For each device, the device parameter is extracted
  3. GraphQL query executes with device variable
  4. Jinja2 template renders with query response
  5. Result is stored as an artifact

Error Handling

Infrahub catches and reports template errors:
from infrahub_sdk.template.exceptions import JinjaTemplateError

try:
    result = await jinja2_template.render(variables=data)
except JinjaTemplateError as exc:
    # Template syntax error, undefined variable, etc.
    raise TransformError(
        repository_name=repo_name,
        commit=commit,
        location=template_path,
        message=exc.message
    )
Common errors:
  • Undefined variables: Accessing data that doesn’t exist
  • Template syntax errors: Invalid Jinja2 syntax
  • Type errors: Attempting invalid operations on data

Best Practices

  1. Check for existence: Always verify data exists before accessing
    {% if intf.node.description.value %}
      description {{ intf.node.description.value }}
    {% endif %}
    
  2. Use meaningful variable names: Make templates readable
    {% for interface in device.interfaces.edges %}
      {# Clear what 'interface' represents #}
    {% endfor %}
    
  3. Add comments: Explain complex logic
    {# Extract loopback IP for OSPF router-id #}
    {% set ns.loopback_ip = intf.node.ip_addresses.edges[0].node.address.value.split('/')[0] %}
    
  4. Keep templates focused: One template per configuration type
  5. Test with sample data: Validate templates before production
  6. Handle missing data gracefully: Provide defaults or skip sections
    {% if intf.node.mtu.value %}
    mtu {{ intf.node.mtu.value }}
    {% endif %}
    
  7. Use consistent indentation: Match output format requirements
  8. Optimize GraphQL queries: Request only needed fields

Template Execution

Templates are rendered using the Infrahub SDK’s Jinja2Template class:
from infrahub_sdk.template import Jinja2Template
from pathlib import Path

jinja2_template = Jinja2Template(
    template=Path("template.j2"),
    template_directory=Path("/repo/templates")
)

rendered = await jinja2_template.render(variables={"data": query_result})
The render process:
  1. Loads template from repository worktree
  2. Validates template syntax
  3. Passes GraphQL query response as data variable
  4. Renders template with Jinja2 engine
  5. Returns rendered string

Timeout Configuration

Jinja2 transforms have configurable timeouts:
jinja2_transforms:
  - name: device_startup
    query: device_startup_info
    template_path: "templates/device_startup_config.tpl.j2"
    timeout: 30  # 30 seconds (default: 10)

Next Steps

Build docs developers (and LLMs) love