NetBox uses Jinja to render configuration templates. Plugins can extend this rendering pipeline in two complementary ways:
jinja_env.py (auto-discovery)Create a file named jinja_env.py in your plugin root and expose a dict called filters. NetBox will auto-discover and register it when the plugin loads.
def prefix_list(device):
"""Return all prefixes assigned to a device's interfaces."""
return [
str(ip.address)
for iface in device.interfaces.all()
for ip in iface.ip_addresses.all()
]
filters = {
'prefix_list': prefix_list,
}
The filter is then available in any config template:
{% for prefix in device | prefix_list %}
network {{ prefix }}
{% endfor %}
register_jinja_filters()You can also register filters programmatically inside your plugin's ready() method:
from netbox.plugins import PluginConfig
class MyPluginConfig(PluginConfig):
name = 'my_plugin'
# ...
def ready(self):
super().ready()
from netbox.plugins.registration import register_jinja_filters
from .jinja_env import filters
register_jinja_filters(filters)
register_jinja_filters() accepts a dict mapping filter names to callables. It raises TypeError if passed a non-dict or if any value is not callable.
The full filter precedence from lowest to highest is: NetBox built-in filters (e.g. env) → plugin-registered filters → instance JINJA_FILTERS. Instance-level filters always win, so site admins can override anything without touching a plugin.
If two plugins register a filter with the same name, the later-loaded plugin's version wins and NetBox will log a warning.
For example, if my_plugin registers a prefix_list filter but a site needs different behaviour, the operator can replace it in configuration.py without touching the plugin:
def prefix_list(device):
# Site-local override: include only loopback prefixes
return [
str(ip.address)
for iface in device.interfaces.filter(type='loopback')
for ip in iface.ip_addresses.all()
]
JINJA_FILTERS = {
'prefix_list': prefix_list,
}
Override get_jinja_context() in your PluginConfig subclass to inject additional variables into every config template render context.
from netbox.plugins import PluginConfig
class MyPluginConfig(PluginConfig):
name = 'my_plugin'
# ...
def get_jinja_context(self):
from .utils import MyNamespace
return {
'my_plugin': MyNamespace(),
}
The returned dict is merged into the template context, so my_plugin becomes available by name inside every config template:
{% set records = my_plugin.lookup(device.name) %}
!!! warning "Startup cost"
`get_jinja_context()` is called on **every** config template render, not once at startup. Keep it fast. Defer expensive lookups to the object you return rather than performing them in `get_jinja_context()` itself.
!!! note "Conflict avoidance"
Choose context variable names that are unlikely to collide with NetBox's built-in template variables (`device`, `queryset`, etc.) or with those contributed by other plugins. Prefixing with your plugin name is strongly recommended.
In addition, avoid top-level app-label names (`dcim`, `ipam`, `virtualization`, etc.). The auto-populated template context maps each app label to a dict of its public model classes; returning a key like `'dcim'` from `get_jinja_context()` will silently replace that entire namespace.
!!! note "No per-render context"
`get_jinja_context()` receives no arguments — it has no access to the object being rendered or the caller-supplied context. It is intended for plugin-global namespaces (e.g. a lazily-evaluated query helper). Per-object logic belongs in the template itself or in a custom filter.