Jinja2 Syntax
Jinja2 templating in NetStacks via the MiniJinja engine -- variables, if/elif/else, for loops, filters, tests, and whitespace control with copy-paste examples.
Overview
NetStacks renders Jinja-style templates with MiniJinja, a Rust implementation of the Jinja2 template language. The syntax you already know from Ansible or Python Jinja2 carries over for the common cases — variable expressions, conditionals, loops, and filters — but MiniJinja is a distinct engine, so some advanced Python-Jinja features are not enabled in the current build. See Supported & Unsupported Features for the exact list.
Jinja templates use three primary syntax elements:
| Syntax | Purpose | Example |
|---|---|---|
{{ }} | Output an expression (variable, filter result, calculation) | {{ hostname }} |
{% %} | Execute a statement (if, for, set) | {% if enable_ospf %} |
{# #} | Comment (not included in rendered output) | {# Configure SNMP settings #} |
MiniJinja is a pure-Rust library. It has no Python interpreter and no access to the file system, network, or shell — not because of a configured sandbox, but because the engine simply does not expose those capabilities. Rendering a template only ever produces text from the template source and the variables you provide.
How It Works
When you render a Jinja template, NetStacks sends a single flat JSON object of variables to the agent, which feeds the template source and those variables into MiniJinja. The engine processes the source top-to-bottom, substitutes expressions, evaluates conditionals, and expands loops. The result is plain text — your device configuration.
The Render Endpoint
Rendering happens over a single endpoint. You POST a variables object and receive the rendered output (or an error message) back:
POST /api/docs/{document_id}/render
Content-Type: application/json{
"variables": {
"hostname": "core-rtr-01",
"domain_name": "example.com",
"dns_servers": ["8.8.8.8", "8.8.4.4"],
"enable_ssh": true
}
}The response always carries a success flag and, on success, the rendered text:
{
"output": "hostname core-rtr-01\nip domain-name example.com\n...",
"success": true,
"error": null
}The Rendering Pipeline
- Load template source — the document content is retrieved; the document must be a Jinja-type document or the request is rejected.
- Parse — MiniJinja parses the source. Syntax errors are caught here and returned as a parse error.
- Bind variables — your single JSON
variablesobject becomes the render context. Objects become maps, arrays become lists, and JSONnullbecomes an undefined value. - Render — the engine walks the template, evaluating expressions and producing output text.
- Return — on success the rendered string comes back; on failure
successisfalseanderrorcontains the MiniJinja message.
There is no shared/per-device/override merge in the render path. Whatever you put in the single variables JSON object is exactly the context the template sees. Build that object however you like before calling the endpoint.
Supported & Unsupported Features
The agent builds MiniJinja with only the json feature and a bare environment that registers a single template with no loader. That choice determines what works.
Supported (enabled by default in MiniJinja)
- Variable expressions:
{{ hostname }}, attribute and index access - Conditionals:
{% if %}/{% elif %}/{% else %}/{% endif %} - Loops:
{% for %}/{% endfor %}, including theloopvariable and{% else %} - Assignment:
{% set x = ... %} - Filters and tests applied with
|andis - Inline conditional expressions:
{{ a if cond else b }} - String concatenation with
~ - Whitespace control with
-(for example{%- ... -%}) - Comments:
{# ... #}
The following are not available because the corresponding MiniJinja features (macros, multi_template) and a template loader are not configured:
{% macro %}/{% call %}(macros){% extends %}/{% block %}(template inheritance){% include %}/{% import %}(other templates)
Templates using these will fail at parse or render time. Keep each template self-contained — everything must live in a single document. If you find yourself wanting macros or inheritance, factor the repeated config into a {% for %} loop over a list, or duplicate the block inline.
Step-by-Step Guide
Build a progressively richer self-contained template by following these steps.
Step 1: Simple Variable Substitution
Start with a basic template that uses variables for device-specific values:
hostname {{ hostname }}
ip domain-name {{ domain_name }}Step 2: Add a Conditional
Use {% if %} to include configuration only when a condition is true:
hostname {{ hostname }}
ip domain-name {{ domain_name }}
{% if enable_ssh %}
ip ssh version 2
ip ssh time-out 60
line vty 0 15
transport input ssh
{% endif %}Step 3: Add a Loop
Use {% for %} to repeat configuration for each item in a list:
{% for server in dns_servers %}
ip name-server {{ server }}
{% endfor %}Step 4: Apply Filters
Filters transform variable output. Chain them with the pipe character:
hostname {{ hostname | upper }}
snmp-server location {{ location | default('Not Configured') }}
snmp-server contact {{ contact | default('[email protected]') | lower }}Step 5: Capture a Value with set
Since macros are not available, use {% set %} to compute a value once and reuse it. This keeps a single template self-contained:
{% set mgmt_desc = 'MGMT - ' ~ site_name | upper %}
interface GigabitEthernet0/0
description {{ mgmt_desc }}
ip address {{ mgmt_ip }} {{ mgmt_mask }}
no shutdownCode Examples
Filters Reference
Common filters for network configuration templates:
{# String transformations #}
{{ hostname | upper }} {# CORE-RTR-01 #}
{{ hostname | lower }} {# core-rtr-01 #}
{{ hostname | capitalize }} {# Core-rtr-01 #}
{{ hostname | replace('-', '_') }} {# core_rtr_01 #}
{{ ('MGMT-' ~ site_name) | trim }} {# string concat with ~ #}
{# Default values for optional variables #}
{{ description | default('No description') }}
{{ mtu | default(1500) }}
{# List operations #}
{{ dns_servers | join(', ') }} {# 8.8.8.8, 8.8.4.4 #}
{{ dns_servers | first }} {# 8.8.8.8 #}
{{ dns_servers | last }} {# 8.8.4.4 #}
{{ dns_servers | length }} {# 2 #}
{# tojson is provided by the json feature #}
{{ vlans | tojson }} {# [{"id":10,...}] #}
{# Inline conditional expression #}
{{ 'enabled' if feature_flag else 'disabled' }}MiniJinja ships its own built-in filter set, which overlaps with but is not identical to Python Jinja2. When in doubt, consult the MiniJinja filter reference rather than the Python Jinja2 docs. The json feature additionally provides the tojson filter.
Conditionals: OSPF Configuration
Enable OSPF only when the variable is set, with optional authentication:
{% if enable_ospf %}
router ospf {{ ospf_process_id | default(1) }}
router-id {{ router_id }}
{% for network in ospf_networks %}
network {{ network.prefix }} {{ network.wildcard }} area {{ network.area }}
{% endfor %}
{% if ospf_auth_enabled | default(false) %}
area {{ ospf_area | default(0) }} authentication message-digest
{% endif %}
{% endif %}Loops: VLAN Configuration
Create multiple VLANs from a list of objects, each with an ID and name:
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
{% endfor %}
!
{% for vlan in vlans %}
interface Vlan{{ vlan.id }}
description {{ vlan.name }} Gateway
ip address {{ vlan.gateway }} {{ vlan.mask }}
no shutdown
{% endfor %}Loop Variable and else
The loop variable exposes the current index and position. A loop {% else %} runs when the list is empty:
{% for acl in access_lists %}
ip access-list extended {{ acl.name }}
{% for rule in acl.rules %}
{{ loop.index }} {{ rule }}
{% endfor %}
{% else %}
{# No ACLs defined for this device #}
{% endfor %}Repeating a Block Without Macros
Macros are not available, so model repeated blocks as a list and loop over it. This produces the same result as calling a macro per interface:
{# variables: { "access_ports": [
{"intf": "GigabitEthernet1/0/1", "vlan": 100, "desc": "Workstation - Finance"},
{"intf": "GigabitEthernet1/0/3", "vlan": 200, "desc": "IP Phone - Sales"}
] } #}
{% for p in access_ports %}
interface {{ p.intf }}
description {{ p.desc }}
switchport mode access
switchport access vlan {{ p.vlan }}
spanning-tree portfast
no shutdown
!
{% endfor %}Questions & Answers
- Q: Which template engine does NetStacks use?
- A: NetStacks renders Jinja templates with MiniJinja, a Rust implementation of Jinja2. The agent builds it with the
jsonfeature on a bare environment with no template loader. - Q: What Jinja features are supported?
- A: Variable expressions, conditionals (
if/elif/else), loops (for/endforwith theloopvariable and loopelse),set, filters, tests (is), inline conditional expressions, string concatenation with~, whitespace control, and comments. See Supported & Unsupported Features. - Q: Can I use macros or template inheritance (extends/block/include)?
- A: No. The current build does not enable MiniJinja's
macrosormulti_templatefeatures, and no template loader is registered, so{% macro %},{% extends %},{% block %}, and{% include %}will fail. Keep each template self-contained; use a{% for %}loop over a list to express repeated config. - Q: What filters are available?
- A: MiniJinja's built-in filter set (for example
upper,lower,capitalize,replace,trim,default,join,first,last,length), plustojsonfrom thejsonfeature. This set overlaps with but is not identical to Python Jinja2, so check the MiniJinja filter reference when unsure. - Q: Can I write custom filters?
- A: Not from the template editor. The render path uses MiniJinja's built-in filters only. If no built-in does what you need, combine existing filters or preprocess the data in the
variablesJSON before sending it to the render endpoint. - Q: Is rendering safe?
- A: Yes. MiniJinja is a pure-Rust library with no Python interpreter and no ability to read files, make network calls, or run shell commands. Rendering only turns the template plus your variables into text.
- Q: How do I handle optional variables?
- A: Use the
defaultfilter for a fallback value ({{ mtu | default(1500) }}). For an entire block that should only appear when a variable exists, guard it with {% if variable_name is defined %} ... {% endif %}. - Q: How do I control whitespace in template output?
- A: Add a dash inside a tag to strip adjacent whitespace:
{%- if ... -%}trims before and after, and{%- for ... -%}removes the blank lines loops otherwise leave. - Q: How do I send variables to render a template?
- A: POST a single flat JSON object to
/api/docs/{document_id}/renderunder thevariableskey. The response returnsoutput,success, anderror.
Troubleshooting
Render error: variable is undefined
This happens when the template references a variable that is not in the variables object (or was passed as JSON null, which becomes undefined). Add the variable or use the default filter:
{# This errors if contact is undefined #}
snmp-server contact {{ contact }}
{# This falls back to the default #}
snmp-server contact {{ contact | default('[email protected]') }}Template parse error: unexpected or missing end tag
Every opening tag needs a matching closing tag. Common mistakes:
{% if %}without{% endif %}{% for %}without{% endfor %}
To check syntax, call POST /api/docs/{document_id}/render with your variables. If the template is invalid, the response comes back with success: false and the MiniJinja message in error. There is no separate validation endpoint — render is the check.
Macro / extends / include fails to render
These constructs are not enabled in the current build (see Supported & Unsupported Features). Remove them and keep the template self-contained — convert repeated blocks into a {% for %} loop over a list.
Filter not found
If you see a "filter not found" error, check spelling (defaults vs default, joins vs join) and confirm the filter exists in MiniJinja — not every Python Jinja2 filter is present. Consult the MiniJinja filter reference.
Extra blank lines in rendered output
Control tags leave blank lines where they appeared. Use whitespace-control dashes to suppress them:
{# Without whitespace control - produces blank lines #}
{% for server in ntp_servers %}
ntp server {{ server }}
{% endfor %}
{# With whitespace control - no extra blank lines #}
{%- for server in ntp_servers %}
ntp server {{ server }}
{%- endfor %}Related Features
Explore related documentation to get the most out of Jinja templates:
- Template Basics — Introduction to templates, the lifecycle, and creating your first template
- Variables & Extraction — How variables are detected in templates and supplied at render time
- Rendering & Preview — Preview rendered output with test values via the render endpoint
- Template Versioning — Track template history and restore previous versions
- Example Templates — Ready-to-adapt templates for VLAN, ACL, BGP, OSPF, and interface configs