Practice Questions

Test your knowledge on this topic in the ENAUTO Exam Trainer — 186 questions across 5 interactive modes.

Jinja2 Templating – Deep Dive for ENAUTO v2.0

Exam Relevance

Exam Topic 3.3 tests Jinja2 constructs: loops, conditionals, filters, and output modifiers. Jinja2 is used in Ansible playbooks, Catalyst Center template programmer, and SD-WAN feature templates.

Exam Topics Covered:
3.3 – Construct Jinja2 templates (loops, conditionals, filters)
3.2 – Python to manage and monitor configurations (template rendering)
3.4 – Ansible controller automation (Jinja2 is the template engine)

Table of Contents


1 – Jinja2 Fundamentals

Jinja2 is a Python-based template engine that separates logic from presentation. In network automation, this means you define a template once and render it with different variable sets to produce device-specific configurations.

┌─────────────────────────────────────────────┐
│              Template (*.j2)                │
│  interface {{ name }}                       │
│   ip address {{ ip }} {{ mask }}            │
└──────────────────┬──────────────────────────┘
                   │ + variables (dict / YAML)
                   ▼
┌─────────────────────────────────────────────┐
│          Jinja2 Engine (Python)             │
│  from jinja2 import Environment             │
│  template.render(variables)                 │
└──────────────────┬──────────────────────────┘
                   │
                   ▼
┌─────────────────────────────────────────────┐
│          Rendered Configuration             │
│  interface GigabitEthernet0/1               │
│   ip address 10.0.0.1 255.255.255.0        │
└─────────────────────────────────────────────┘

Delimiters

Jinja2 uses three delimiter types to distinguish template logic from literal text:

DelimiterPurposeExample
{{ ... }}Expression — output a variable or expression{{ hostname }}
{% ... %}Statement — logic (for, if, set, block, macro){% for vlan in vlans %}
{# ... #}Comment — ignored in output{# TODO: add ACL #}

Variable Access

Variables can be accessed with dot notation or bracket notation:

{# Both are equivalent #}
{{ interface.name }}
{{ interface['name'] }}

When to Use Bracket Notation

Use bracket notation when the key contains special characters, starts with a digit, or is a Python reserved word:

{{ interface['802.1Q'] }}
{{ device['class'] }}

Undefined Variables

By default, Jinja2 prints an empty string for undefined variables — this is dangerous for network configs because a missing IP address produces invalid output silently.

from jinja2 import Environment, StrictUndefined
 
# Default: undefined → empty string (dangerous)
env = Environment()
 
# Strict mode: undefined → raises UndefinedError (recommended)
env = Environment(undefined=StrictUndefined)

Exam Trap

Ansible uses Jinja2 in strict mode by default — referencing an undefined variable will fail the playbook. Standard Jinja2 in Python does not.

Minimal Python Rendering Example

from jinja2 import Environment, FileSystemLoader
 
# Load templates from ./templates/ directory
env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("router.j2")
 
variables = {
    "hostname": "BRANCH-RTR-01",
    "interfaces": [
        {"name": "GigabitEthernet0/0", "ip": "10.0.0.1", "mask": "255.255.255.0"},
        {"name": "GigabitEthernet0/1", "ip": "10.0.1.1", "mask": "255.255.255.0"},
    ]
}
 
config = template.render(variables)
print(config)

2 – Loops

Basic {% for %} Loop

The for loop iterates over a list and renders a block for each item:

{% for interface in interfaces %}
interface {{ interface.name }}
 description {{ interface.description }}
 ip address {{ interface.ip }} {{ interface.mask }}
!
{% endfor %}

Given this data:

interfaces:
  - name: GigabitEthernet0/0
    description: UPLINK
    ip: 10.0.0.1
    mask: 255.255.255.0
  - name: GigabitEthernet0/1
    description: LAN
    ip: 10.0.1.1
    mask: 255.255.255.0

Output:

interface GigabitEthernet0/0
 description UPLINK
 ip address 10.0.0.1 255.255.255.0
!
interface GigabitEthernet0/1
 description LAN
 ip address 10.0.1.1 255.255.255.0
!

Loop Variables

Inside a for loop, Jinja2 provides special variables via the loop object:

VariableDescriptionExample Value
loop.indexCurrent iteration (1-based)1, 2, 3
loop.index0Current iteration (0-based)0, 1, 2
loop.firstTrue if first iterationTrue / False
loop.lastTrue if last iterationTrue / False
loop.lengthTotal number of items3
loop.revindexIterations remaining (1-based)3, 2, 1
loop.previtemItem from previous iteration(undefined on first)
loop.nextitemItem from next iteration(undefined on last)
{% for server in ntp_servers %}
ntp server {{ server }}{% if loop.last %} prefer{% endif %}
 
{% endfor %}

Output:

ntp server 10.0.0.10
ntp server 10.0.0.11
ntp server 10.0.0.12 prefer

Nested Loops

For interfaces with sub-interfaces:

{% for intf in interfaces %}
interface {{ intf.name }}
 description {{ intf.description }}
{% for sub in intf.subinterfaces %}
interface {{ intf.name }}.{{ sub.id }}
 encapsulation dot1Q {{ sub.vlan }}
 ip address {{ sub.ip }} {{ sub.mask }}
{% endfor %}
{% endfor %}

Loop Filtering

Filter items inline with an if clause on the for statement:

{% for intf in interfaces if intf.enabled %}
interface {{ intf.name }}
 no shutdown
{% endfor %}

Loop Filtering vs. Conditional Inside Loop

{% for intf in interfaces if intf.enabled %} is cleaner than putting an {% if %} inside the loop body. It also correctly sets loop.length to only count matching items.

Empty Loop ({% else %})

The {% else %} block executes when the loop iterable is empty:

{% for vlan in vlans %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
{% else %}
! No VLANs defined
{% endfor %}

Example: VLAN Configuration for a Switch

{# Template: switch_vlans.j2 #}
{% for vlan in vlans %}
vlan {{ vlan.id }}
 name {{ vlan.name | default('VLAN' ~ vlan.id) }}
{% if vlan.description is defined %}
 description {{ vlan.description }}
{% endif %}
{% endfor %}
!
{% for vlan in vlans if vlan.svi is defined %}
interface Vlan{{ vlan.id }}
 ip address {{ vlan.svi.ip }} {{ vlan.svi.mask }}
 no shutdown
{% endfor %}

3 – Conditionals

Basic {% if %} / {% elif %} / {% else %}

{% if interface.type == 'trunk' %}
switchport mode trunk
switchport trunk allowed vlan {{ interface.vlans | join(',') }}
{% elif interface.type == 'access' %}
switchport mode access
switchport access vlan {{ interface.vlan }}
{% else %}
switchport mode dynamic desirable
{% endif %}

Boolean Tests

TestMeaning
is definedVariable exists
is undefinedVariable does not exist
is noneVariable is None
is trueVariable evaluates to True
is falseVariable evaluates to False
{% if ntp_servers is defined %}
{% for server in ntp_servers %}
ntp server {{ server }}
{% endfor %}
{% endif %}

Comparison Operators

OperatorMeaning
==Equal
!=Not equal
>Greater than
<Less than
>=Greater than or equal
<=Less than or equal

Logical Operators

OperatorExample
and{% if vlan.id > 1 and vlan.id < 4094 %}
or{% if role == 'core' or role == 'distribution' %}
not{% if not interface.shutdown %}

is Tests

{% if hostname is string %}
hostname {{ hostname }}
{% endif %}
 
{% if vlans is iterable %}
{% for vlan in vlans %}
vlan {{ vlan }}
{% endfor %}
{% endif %}
 
{% if mtu is number %}
mtu {{ mtu }}
{% endif %}

Example: Conditional ACL Generation Based on Security Level

{# Template: acl_config.j2 #}
{% if security_level == 'high' %}
ip access-list extended SECURE-ACL
 deny   ip any any log
 permit tcp any host {{ mgmt_server }} eq 443
 permit tcp any host {{ mgmt_server }} eq 22
{% elif security_level == 'medium' %}
ip access-list extended STANDARD-ACL
 permit ip {{ trusted_network }} {{ trusted_wildcard }} any
 deny   ip any any log
{% else %}
ip access-list extended OPEN-ACL
 permit ip any any
{% endif %}
!
{% for intf in interfaces if intf.acl is defined %}
interface {{ intf.name }}
 ip access-group {{ intf.acl }} {{ intf.acl_direction | default('in') }}
{% endfor %}

4 – Filters

Filters are pipe-based transformations applied to variables. They are the Jinja2 equivalent of Unix pipes.

{{ variable | filter_name }}
{{ variable | filter_name(arg1, arg2) }}

Essential Filters for Network Automation

String Filters

FilterPurposeExampleOutput
upperUppercase{{ "hello" | upper }}HELLO
lowerLowercase{{ "HELLO" | lower }}hello
capitalizeCapitalize first letter{{ "hello world" | capitalize }}Hello world
titleTitle Case{{ "hello world" | title }}Hello World
replaceReplace substring{{ "Gi0/0" | replace("Gi", "GigabitEthernet") }}GigabitEthernet0/0
trimStrip whitespace{{ " hello " | trim }}hello
truncateLimit length{{ description | truncate(30) }}First 30 chars + ...

Default Value

{# Provide fallback for undefined variables #}
hostname {{ hostname | default('ROUTER') }}
 
{# Also replace empty strings (second argument = true) #}
description {{ description | default('NO DESCRIPTION', true) }}

Exam Trap

| default('value') only replaces undefined variables. To also replace empty strings and None, you must pass true as the second argument: | default('value', true).

Type Conversion

FilterPurposeExample
intConvert to integer{{ "100" | int }}
floatConvert to float{{ "3.14" | float }}
stringConvert to string{{ 100 | string }}

List Filters

FilterPurposeExampleOutput
join(',')Join list elements{{ [10,20,30] | join(',') }}10,20,30
lengthCount items{{ vlans | length }}3
sortSort ascending{{ [3,1,2] | sort }}[1, 2, 3]
reverseReverse order{{ [1,2,3] | reverse | list }}[3, 2, 1]
uniqueRemove duplicates{{ [1,1,2,3] | unique | list }}[1, 2, 3]
firstFirst item{{ [10,20,30] | first }}10
lastLast item{{ [10,20,30] | last }}30
minMinimum value{{ [10,20,30] | min }}10
maxMaximum value{{ [10,20,30] | max }}30

Object / Attribute Filters

These are critical for working with lists of dictionaries (e.g., device inventories):

{# Select objects where enabled == true #}
{{ interfaces | selectattr('enabled', 'eq', true) | list }}
 
{# Reject objects where shutdown == true #}
{{ interfaces | rejectattr('shutdown', 'eq', true) | list }}
 
{# Extract a single attribute from each object #}
{{ interfaces | map(attribute='name') | list }}

Filter Chaining

Filters can be chained to build powerful one-liners:

{{ interfaces | selectattr('enabled') | map(attribute='name') | join(', ') }}

This selects enabled interfaces, extracts their names, and joins them with commas.

Ansible-Only Filters

These filters are not available in standard Jinja2 — they are added by Ansible:

FilterPurposeExample
regex_replace(pattern, repl)Regex substitution{{ hostname | regex_replace('^(.+)-\\d+$', '\\1') }}
regex_search(pattern)Regex match{{ output | regex_search('Version (\\S+)') }}
ipaddrIP address manipulation{{ '10.0.0.1/24' | ipaddr('network') }}
ipv4Filter IPv4 addresses{{ addresses | ipv4 }}
ipv6Filter IPv6 addresses{{ addresses | ipv6 }}
to_jsonSerialize to JSON{{ data | to_json }}
to_yamlSerialize to YAML{{ data | to_yaml }}
to_nice_jsonPretty-print JSON{{ data | to_nice_json(indent=2) }}
from_jsonParse JSON string{{ json_string | from_json }}

Exam Trap

ipaddr, to_json, to_yaml, and regex_replace are Ansible-specific filters. They require the ansible.utils or ansible.netcommon collections. Using them in pure Python Jinja2 will raise an error.

Example: NTP Server Config with Fallback Defaults

{# Template: ntp_config.j2 #}
{% set servers = ntp_servers | default(['10.0.0.10', '10.0.0.11']) %}
{% for server in servers %}
ntp server {{ server }}{% if loop.first %} prefer{% endif %}
 
{% endfor %}
ntp source {{ ntp_source_interface | default('Loopback0') }}

Python rendering:

from jinja2 import Environment, FileSystemLoader
 
env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("ntp_config.j2")
 
# Render with custom servers
print(template.render(ntp_servers=["172.16.0.1", "172.16.0.2"]))
 
# Render with defaults (no variables passed)
print(template.render())

5 – Whitespace Control

The Problem

Jinja2 blocks produce blank lines in the output because the block tags themselves occupy a line:

{% for vlan in vlans %}
vlan {{ vlan.id }}
{% endfor %}

Renders as (note the blank lines):


vlan 10

vlan 20

Solutions

Tag-Level Whitespace Stripping

Add a dash (-) inside the delimiter to strip whitespace:

SyntaxEffect
{%- ... %}Strip whitespace before the tag
{% ... -%}Strip whitespace after the tag
{%- ... -%}Strip whitespace on both sides
{%- for vlan in vlans %}
vlan {{ vlan.id }}
{%- endfor %}

Clean output:

vlan 10
vlan 20

Environment-Level Settings

env = Environment(
    loader=FileSystemLoader("templates"),
    trim_blocks=True,      # Remove first newline after a block tag
    lstrip_blocks=True,    # Strip leading whitespace before block tags
)

Best Practice

Set trim_blocks=True and lstrip_blocks=True at the Environment level. This produces clean output without littering your templates with dashes. Use manual dashes ({%-, -%}) only for fine-tuning edge cases.

Before vs After Comparison

Without whitespace control:

hostname {{ hostname }}
!
{% for intf in interfaces %}
interface {{ intf.name }}
 ip address {{ intf.ip }} {{ intf.mask }}
{% if intf.description is defined %}
 description {{ intf.description }}
{% endif %}
!
{% endfor %}
hostname BRANCH-01
!

interface Gi0/0
 ip address 10.0.0.1 255.255.255.0

 description UPLINK

!

interface Gi0/1
 ip address 10.0.1.1 255.255.255.0

!

With trim_blocks=True and lstrip_blocks=True:

hostname BRANCH-01
!
interface Gi0/0
 ip address 10.0.0.1 255.255.255.0
 description UPLINK
!
interface Gi0/1
 ip address 10.0.1.1 255.255.255.0
!

6 – Template Inheritance and Includes

Template Inheritance (extends / block)

Parent template (base_router.j2):

! ===== Base Router Configuration =====
service timestamps debug datetime msec
service timestamps log datetime msec
!
hostname {{ hostname }}
!
{% block interfaces %}
! No interfaces defined
{% endblock %}
!
{% block routing %}
! No routing defined
{% endblock %}
!
{% block services %}
no ip http server
ip ssh version 2
{% endblock %}
!
end

Child template (branch_router.j2):

{% extends "base_router.j2" %}
 
{% block interfaces %}
{% for intf in interfaces %}
interface {{ intf.name }}
 ip address {{ intf.ip }} {{ intf.mask }}
 no shutdown
{% endfor %}
{% endblock %}
 
{% block routing %}
router ospf 1
 router-id {{ router_id }}
{% for network in ospf_networks %}
 network {{ network.subnet }} {{ network.wildcard }} area {{ network.area }}
{% endfor %}
{% endblock %}

When to Use Inheritance

Use extends when you have a base configuration that is shared across device roles (core, distribution, access) with only certain sections changing. The child template only overrides the blocks it needs.

Includes

Include a snippet inside another template:

{# main_config.j2 #}
hostname {{ hostname }}
!
{% include "acl_config.j2" %}
!
{% include "ntp_config.j2" %}

Macros

Macros are reusable template functions:

{% macro interface_config(name, ip, mask, description='') %}
interface {{ name }}
{% if description %}
 description {{ description }}
{% endif %}
 ip address {{ ip }} {{ mask }}
 no shutdown
{% endmacro %}
 
{# Usage #}
{{ interface_config('GigabitEthernet0/0', '10.0.0.1', '255.255.255.0', 'UPLINK') }}
{{ interface_config('GigabitEthernet0/1', '10.0.1.1', '255.255.255.0') }}

Macros vs Includes

  • Macros: best for repeated config blocks with different parameters (like interface configs)
  • Includes: best for static or semi-static config sections (like banner, NTP, logging)

7 – Jinja2 in Catalyst Center Template Programmer

Catalyst Center’s Template Programmer allows you to create and deploy configuration templates to managed devices. It supports both Velocity and Jinja2 syntax.

Velocity vs Jinja2 Syntax

FeatureVelocityJinja2
Variable$variable or ${variable}{{ variable }}
If#if($x == "y"){% if x == "y" %}
For#foreach($item in $list){% for item in list %}
End block#end{% endif %} / {% endfor %}

Exam Trap

Catalyst Center supports both Velocity and Jinja2. When you create a template, you must select the language. Mixing syntax (e.g., using $variable in a Jinja2 template) will fail. The exam may show code and ask which template language is being used.

Template Deployment via API

Reference: 9 – Jinja2 Configuration Templates

import requests
from requests.auth import HTTPBasicAuth
 
BASE_URL = "https://10.10.20.85"
 
# 1. Authenticate
token_resp = requests.post(
    f"{BASE_URL}/dna/system/api/v1/auth/token",
    auth=HTTPBasicAuth("admin", "C1sco12345"),
    verify=False
)
token = token_resp.json()["Token"]
headers = {"X-Auth-Token": token, "Content-Type": "application/json"}
 
# 2. Create a Jinja2 template
template_payload = {
    "name": "VLAN_Provisioning",
    "description": "Day-N VLAN provisioning template",
    "language": "JINJA",   # JINJA or VELOCITY
    "projectName": "Onboarding",
    "templateContent": """
vlan {{ vlan_id }}
 name {{ vlan_name | default('DATA') }}
!
interface Vlan{{ vlan_id }}
 ip address {{ svi_ip }} {{ svi_mask }}
 no shutdown
""",
    "deviceTypes": [{"productFamily": "Switches and Hubs"}],
    "softwareType": "IOS-XE",
    "templateParams": [
        {"parameterName": "vlan_id", "dataType": "INTEGER", "required": True},
        {"parameterName": "vlan_name", "dataType": "STRING", "required": False},
        {"parameterName": "svi_ip", "dataType": "STRING", "required": True},
        {"parameterName": "svi_mask", "dataType": "STRING", "required": True},
    ]
}
 
resp = requests.post(
    f"{BASE_URL}/dna/intent/api/v1/template-programmer/project/Onboarding/template",
    headers=headers,
    json=template_payload,
    verify=False
)
task_id = resp.json()["response"]["taskId"]
print(f"Template creation task: {task_id}")

Async Operations

Template creation in Catalyst Center is asynchronous. The API returns a taskId — poll /dna/intent/api/v1/task/{taskId} until completion. See Catalyst_Center_Deep_Dive for the polling pattern.

Day-N Template for VLAN Provisioning

{# Catalyst Center Jinja2 Template #}
{# Variables: vlan_id (int), vlan_name (str), svi_ip (str), svi_mask (str) #}
{% if vlan_id is defined and svi_ip is defined %}
vlan {{ vlan_id }}
 name {{ vlan_name | default('DATA-VLAN') }}
!
interface Vlan{{ vlan_id }}
 description Auto-provisioned by Catalyst Center
 ip address {{ svi_ip }} {{ svi_mask }}
 no shutdown
{% endif %}

8 – Jinja2 in SD-WAN Feature Templates

SD-WAN (vManage) uses Jinja2 for CLI add-on templates that extend feature templates with custom CLI commands.

Reference: 6 – Feature Templates and Device Templates

Variable Types in SD-WAN Templates

TypeBehaviorUse Case
ConstantSame value for all devicesOrganization name, NTP servers
VariableSet per device at attachmentHostname, IP addresses
IgnoreUse device defaultFeatures left at default
Device-specificPrompted when attaching templateSite-specific values

CLI Add-On Template Example

{# SD-WAN CLI Add-On Template #}
{% if enable_netflow is defined and enable_netflow %}
flow exporter EXPORT-TO-COLLECTOR
 destination {{ collector_ip }}
 transport udp {{ collector_port | default(2055) }}
 source Loopback0
!
flow monitor NETFLOW-MONITOR
 exporter EXPORT-TO-COLLECTOR
 record netflow ipv4 original-input
!
{% for intf in monitored_interfaces %}
interface {{ intf }}
 ip flow monitor NETFLOW-MONITOR input
 ip flow monitor NETFLOW-MONITOR output
{% endfor %}
{% endif %}

SD-WAN Template Binding

In SD-WAN, feature templates define individual features (AAA, VPN, interface settings). Multiple feature templates are combined into a device template, which is then attached to a device. CLI add-on templates (Jinja2) supplement the feature templates with commands not covered by the GUI.


9 – Jinja2 in Ansible Playbooks

Ansible uses Jinja2 natively throughout its entire ecosystem. Every Ansible YAML file is processed through the Jinja2 engine before execution.

Reference: 3.4_Ansible_Controller_Automation

Where Jinja2 Appears in Ansible

ContextExample
template moduletemplate: src=config.j2 dest=/tmp/config.txt
Variable expressions{{ hostvars[inventory_hostname].ansible_host }}
when clauseswhen: ansible_network_os == 'cisco.ios.ios'
Loop expressionsloop: "{{ vlans | selectattr('enabled') | list }}"
set_factset_fact: uplink="{{ interfaces | first }}"

Template Module Example

Playbook (deploy_config.yml):

---
- name: Deploy switch configuration
  hosts: access_switches
  gather_facts: false
  vars:
    vlans:
      - id: 10
        name: DATA
      - id: 20
        name: VOICE
      - id: 99
        name: MGMT
 
  tasks:
    - name: Generate VLAN config from template
      ansible.builtin.template:
        src: vlans.j2
        dest: "/tmp/{{ inventory_hostname }}_vlans.cfg"
 
    - name: Push config to device
      cisco.ios.ios_config:
        src: "/tmp/{{ inventory_hostname }}_vlans.cfg"

Template (vlans.j2):

{% for vlan in vlans %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
{% endfor %}

Ansible-Specific Jinja2 Patterns

# Using ipaddr filter (requires ansible.utils collection)
- name: Set network address
  set_fact:
    network: "{{ '10.0.0.1/24' | ipaddr('network/prefix') }}"
    # Result: 10.0.0.0/24
 
# Using regex_search
- name: Extract version from show output
  set_fact:
    ios_version: "{{ show_version.stdout[0] | regex_search('Version (\\S+)', '\\1') | first }}"
 
# Using to_json for API calls
- name: Send payload to controller
  uri:
    url: "https://catalyst-center/api/endpoint"
    body: "{{ payload | to_json }}"
    body_format: json

Jinja2 in when Conditions

# Note: when clauses are already inside Jinja2 context
# Do NOT add {{ }} around the expression
- name: Configure trunk ports only
  cisco.ios.ios_config:
    lines:
      - switchport mode trunk
  when: interface_type == 'trunk'    # Correct
  # when: "{{ interface_type == 'trunk' }}"  # WRONG — double evaluation

Exam Trap

In Ansible when clauses, do not wrap the expression in {{ }}. The when keyword already evaluates its value as a Jinja2 expression. Adding braces causes double evaluation and may produce unexpected results.


10 – Common Patterns and Gotchas

Top Exam Traps

1. Undefined Variables

{# Dangerous — renders as empty string in default Jinja2 #}
hostname {{ hostname }}
 
{# Safe — provides a fallback #}
hostname {{ hostname | default('ROUTER') }}

2. Whitespace in Output

Always test rendered output. Uncontrolled whitespace produces invalid IOS configs. Use trim_blocks=True and lstrip_blocks=True.

3. Velocity vs Jinja2 Confusion

The exam may show a Catalyst Center template and ask what is wrong. Watch for:

  • $variable syntax in a Jinja2 template (wrong)
  • {{ variable }} syntax in a Velocity template (wrong)
  • Missing {% endif %} or {% endfor %} (Jinja2 requires explicit end tags; Velocity uses #end)

4. Filter Availability

FilterStandard Jinja2Ansible
defaultYesYes
joinYesYes
upper / lowerYesYes
selectattrYesYes
mapYesYes
ipaddrNoYes (ansible.utils)
to_json / to_yamlNoYes
regex_replaceNoYes

5. set Scope

Variables created with {% set %} inside a for loop are scoped to that loop — they do not persist outside.

{% set count = 0 %}
{% for vlan in vlans %}
  {% set count = count + 1 %}  {# This count is loop-scoped! #}
{% endfor %}
{{ count }}  {# Still 0 in standard Jinja2 #}

Workaround for Scoping

Use a namespace object (Jinja2 2.10+):

{% set ns = namespace(count=0) %}
{% for vlan in vlans %}
  {% set ns.count = ns.count + 1 %}
{% endfor %}
{{ ns.count }}  {# Correct! #}

6. String Concatenation

Use the ~ operator for concatenation in Jinja2, not +:

{{ "interface " ~ intf_name }}
{# Or simply use adjacent expressions: #}
interface {{ intf_name }}

11 – Exam-Style Scenarios

Scenario 1: Predict the Output

Given this template and data, what is the rendered output?

{% for intf in interfaces if intf.enabled %}
interface {{ intf.name }}
 ip address {{ intf.ip }} {{ intf.mask }}
{% else %}
! No enabled interfaces found
{% endfor %}
interfaces:
  - name: Gi0/0
    ip: 10.0.0.1
    mask: 255.255.255.0
    enabled: false
  - name: Gi0/1
    ip: 10.0.1.1
    mask: 255.255.255.0
    enabled: false

Scenario 2: Filter Chaining

What does this expression produce?

{{ vlans | selectattr('active') | map(attribute='id') | sort | join(', ') }}
vlans:
  - id: 30
    active: true
  - id: 10
    active: true
  - id: 20
    active: false
  - id: 40
    active: true

Scenario 3: Default Filter

A network engineer writes this template. What happens when description is an empty string ""?

interface GigabitEthernet0/0
 description {{ description | default('NO DESCRIPTION') }}

Scenario 4: Velocity vs Jinja2

A developer creates this Catalyst Center template and selects “Jinja2” as the language. What is wrong?

hostname $hostname
#if($enable_ssh)
ip ssh version 2
line vty 0 15
 transport input ssh
#end

Scenario 5: Ansible when Clause

What is wrong with this Ansible task?

- name: Configure trunk ports
  cisco.ios.ios_config:
    lines:
      - switchport mode trunk
  when: "{{ interface_mode == 'trunk' }}"

Scenario 6: Loop Variable

What config does this template produce?

{% for server in dns_servers %}
ip name-server {{ server }}{% if not loop.last %} priority {{ loop.index }}{% endif %}
{% endfor %}
dns_servers:
  - 8.8.8.8
  - 8.8.4.4
  - 1.1.1.1

12 – Quick Reference Cheat Sheet

Delimiters

SyntaxPurpose
{{ expression }}Output a value
{% statement %}Logic (for, if, set, block, macro)
{# comment #}Comment (not in output)
{%- ... -%}Strip surrounding whitespace

Common Filters

FilterPurposeStandardAnsible
default(val)Fallback valueYesYes
default(val, true)Fallback for empty + undefinedYesYes
join(sep)Join listYesYes
int / float / stringType castYesYes
upper / lower / titleCase conversionYesYes
replace(old, new)String replaceYesYes
lengthCount itemsYesYes
sort / reverseOrderingYesYes
uniqueDeduplicateYesYes
first / lastGet first/last itemYesYes
selectattr(key, test, val)Filter objectsYesYes
rejectattr(key, test, val)Reject objectsYesYes
map(attribute=key)Extract attributeYesYes
ipaddr / ipv4 / ipv6IP manipulationNoYes
to_json / to_yamlSerializationNoYes
regex_replace / regex_searchRegexNoYes

Loop Variables

VariableTypeDescription
loop.indexintCurrent iteration (1-based)
loop.index0intCurrent iteration (0-based)
loop.firstboolTrue on first iteration
loop.lastboolTrue on last iteration
loop.lengthintTotal items in loop
loop.revindexintIterations remaining (1-based)

Whitespace Control

SyntaxEffect
{%- ... %}Strip before tag
{% ... -%}Strip after tag
{%- ... -%}Strip both sides
trim_blocks=TrueAuto-remove newline after block tag
lstrip_blocks=TrueAuto-strip leading whitespace before block

Key Differences: Velocity vs Jinja2 (Catalyst Center)

FeatureVelocityJinja2
Variable$var / ${var}{{ var }}
Conditional#if(...) #end{% if ... %} {% endif %}
Loop#foreach($i in $list) #end{% for i in list %} {% endfor %}
Comment## comment{# comment #}

Exam Topic: 3.3 | Last updated: 2026-03-07 Related: Catalyst_Center_Deep_Dive · SDWAN_Deep_Dive · 3.4_Ansible_Controller_Automation · ENAUTOv2_API_Learning_Plan