NetStacksNetStacks

Variables & Extraction

How NetStacks discovers Jinja2 variables from a template to build the render form, and how you supply a flat JSON object of values at render time.

Overview

Variables are the bridge between a generic template and a device-specific configuration. When you write {{ hostname }} or {{ vlan_id }} in a Jinja2 template, you are declaring a placeholder that is replaced with a real value when the template is rendered.

NetStacks works with variables in two stages:

  • Client-side extraction — When you open the render dialog for a Jinja template, the app parses the template text and discovers the variable names so it can build an input form for you. Extraction happens in the browser and is not persisted on the document.
  • Value substitution at render time — You supply a flat JSON object of values. The agent renders the template with the minijinja engine and returns the finished text.
Free in NetStacks Terminal

Template rendering and variable extraction are built-in features of NetStacks Terminal. No license, controller, or special tier is required.

How Variable Extraction Works

When you trigger a render, the template text is passed to a client-side extractor (extractJinjaVariables). The extractor scans the template line by line and returns one entry per discovered variable, each with a name, the line it was found on, any filters applied, and an isLoop flag.

It looks for variables in three places:

  • Output expressions — anything inside {{ ... }}, for example {{ hostname }} or {{ hostname | upper }}.
  • For-loop sources — the iterable in a {% for x in items %} block (items here). These are flagged as arrays.
  • If-condition identifiers — the variable names referenced in a {% if ... %} condition, for example enable_ospf in {% if enable_ospf %}.

For each variable the dialog calls inferVariableType to guess a type (string, number, boolean, or array) so it can show the right kind of input. The inference uses the isLoop flag and the filters applied to the expression — it is a UI hint only and does not change how the value is rendered.

Extraction is not stored

The discovered variable list is computed fresh each time you open the render dialog. It is not saved to the document, so editing a template instantly changes the variables you are prompted for — there is nothing to re-sync or re-index.

Extraction Rules & Nuances

Knowing exactly what the extractor does (and does not) detect avoids surprises when filling in the render form.

Only the root name is captured

For an expression like {{ vlan.id }} or {{ vlans[0].name }}, the extractor keeps only the root token before the first dot or bracket —vlan and vlans respectively. You supply the whole object (or array of objects), and Jinja2 reaches into its properties at render time.

Loop iteration variables are skipped

In {% for server in ntp_servers %}{{ server }}{% endfor %} the iteration variable server is intentionally excluded — it is produced by the loop, not supplied by you. The loop source, ntp_servers, is captured and flagged with isLoop: trueso the form renders it as an array (multi-value) input.

Jinja keywords are ignored

Inside if-conditions, language keywords and literals such as and, or, not, in, is, true, false, and none are filtered out so they never appear as variables to fill in.

Each name appears once

A variable used many times is reported a single time. The first occurrence wins for the recorded line number and filters.

Use double braces for output

Only {{ ... }} output tags, {% for %} sources, and {% if %}conditions are scanned. A value written with single braces { hostname } is not Jinja2 syntax and will be emitted literally — it will not be extracted or substituted.

Step-by-Step Guide

Walk through the full variable workflow from authoring a template to rendering it with values.

Step 1: Write a template with variables

Create a Jinja template and use {{ variable_name }} placeholders wherever device-specific values belong:

router-base.j2jinja2
hostname {{ hostname }}
!
interface Loopback0
 ip address {{ router_id }} 255.255.255.255
!
router ospf 1
 router-id {{ router_id }}
 network {{ mgmt_network }} {{ mgmt_wildcard }} area 0
!
{% for server in ntp_servers %}
ntp server {{ server }}
{% endfor %}

Step 2: Open the render dialog and review extracted variables

When you render this template, the extractor discovers the variables below. Note that the loop iteration variable server is omitted, while its source ntp_servers is treated as an array:

extracted-variables.txttext
hostname        (string)
router_id       (string)
mgmt_network    (string)
mgmt_wildcard   (string)
ntp_servers     (array)   # loop source -> multi-value input

Step 3: Enter values

Fill in a value for each scalar variable and add one or more entries for the array variable. The dialog collects everything into a single flat JSON object.

Step 4: Render

The agent renders the template with minijinja and returns the finished configuration text, which you can copy or paste into a device session.

Code Examples

Template with several variable patterns

multi-variable-patterns.j2jinja2
{# Patterns: simple, filtered, default, loop, nested object #}

hostname {{ hostname | upper }}
ip domain-name {{ domain | default('lab.local') }}
!
{% for nameserver in dns_servers %}
ip name-server {{ nameserver }}
{% endfor %}
!
{% for vlan in vlans %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
{% endfor %}
!
{% if enable_snmp %}
snmp-server community {{ snmp_community }} RO
snmp-server location {{ site_info.building }}, {{ site_info.floor }}
{% endif %}

Variables the extractor reports (root names only, iteration variables excluded): hostname, domain, dns_servers (array), vlans (array), enable_snmp, snmp_community, site_info.

Render request

The render endpoint takes the document id in the path and a single flat variables object in the body:

render-request.jsonjson
POST /api/docs/{id}/render
Content-Type: application/json

{
  "variables": {
    "hostname": "DIST-SW-01",
    "domain": "corp.example.com",
    "dns_servers": ["10.0.0.53", "10.0.0.54"],
    "vlans": [
      { "id": 100, "name": "USERS" },
      { "id": 200, "name": "VOICE" },
      { "id": 300, "name": "PRINTERS" }
    ],
    "enable_snmp": true,
    "snmp_community": "public-ro",
    "site_info": { "building": "HQ-East", "floor": "3rd Floor" }
  }
}

Render response

The agent returns the rendered text along with a success flag and an optional error string:

render-response.jsonjson
{
  "output": "hostname DIST-SW-01\nip domain-name corp.example.com\n!\nip name-server 10.0.0.53\nip name-server 10.0.0.54\n!\nvlan 100\n name USERS\nvlan 200\n name VOICE\nvlan 300\n name PRINTERS\n!\nsnmp-server community public-ro RO\nsnmp-server location HQ-East, 3rd Floor",
  "success": true,
  "error": null
}

Rendered output

The output field, formatted:

rendered-output.txttext
hostname DIST-SW-01
ip domain-name corp.example.com
!
ip name-server 10.0.0.53
ip name-server 10.0.0.54
!
vlan 100
 name USERS
vlan 200
 name VOICE
vlan 300
 name PRINTERS
!
snmp-server community public-ro RO
snmp-server location HQ-East, 3rd Floor

Error response

When rendering fails (for example a missing required variable), the agent still returns HTTP 200 with success: false and a message in error:

render-error.jsonjson
{
  "output": "",
  "success": false,
  "error": "Render error: undefined value (in template:1)"
}

Questions & Answers

Q: How are variables extracted from templates?
A: The render dialog parses the template text in the browser (extractJinjaVariables) and collects every variable referenced in {{ }} output tags, {% for %} loop sources, and {% if %} conditions. This builds the input form. The list is computed on the fly and is not stored on the document.
Q: How do I supply values when rendering?
A: Send a single flat JSON object under a variables key to POST /api/docs/{id}/render. Keys are the variable names; values can be strings, numbers, booleans, arrays, or nested objects. The agent renders with minijinja and returns { output, success, error }.
Q: Are loop variables prompted for?
A: No. In {% for server in ntp_servers %} the iteration variable server is excluded because the loop produces it. The loop source ntp_servers is prompted as an array, and you provide the list of items.
Q: How do nested object variables like {{ interface.ip }} work?
A: The extractor captures only the root token interface. You then supply an object with an ip property, and Jinja2 resolves the dot path at render time. The same applies to bracket access such as {{ vlans[0].name }} — the root is vlans.
Q: How do I set default values?
A: Use the Jinja2 default filter directly in the template, for example {{ mtu | default(1500) }}. This supplies a fallback when the variable is omitted from the variables object.
Q: What happens if a required variable is missing?
A: minijinja treats it as undefined. If the template uses that value in a way that requires it, rendering fails and the response comes back with success: false and a Render error message identifying the problem. Add a default filter for any variable that may be optional.
Q: Do I need a license or the controller to render templates?
A: No. Variable extraction and rendering are free features of NetStacks Terminal and work entirely on the local agent.

Troubleshooting

A variable is not appearing in the render form

The extractor only inspects Jinja2 tags. Common causes:

  • Using single braces { hostname } instead of double braces {{ hostname }} — single braces are not Jinja2 syntax.
  • The name is a loop iteration variable (the x in {% for x in items %}). These are skipped by design; supply the loop source instead.
  • The token is a Jinja2 keyword or literal (true, and, in, and so on) inside an if-condition — keywords are filtered out.

Nested object variables are not listed individually

This is expected. For {{ vlan.id }} the form prompts for vlan, not vlan.id. Provide a full object (or array of objects) whose properties match every path your template references.

Rendering fails with an undefined-value error

The response will have success: false and an error such as Render error: undefined value. Make sure every variable the template uses is present in the variables object, or add a default filter in the template so optional values fall back gracefully.

An array variable renders incorrectly

A loop source must be a JSON array. If you pass a single scalar where the template loops with {% for x in items %}, the loop body may not run as expected. In the render form, array-typed inputs accept multiple entries — add one row per item.

Learn more about working with templates: