Nunjucks Patterns
Witboost templates use Nunjucks as the templating language for catalog-info.yaml and other skeleton files. This guide covers the most common patterns you'll encounter and write in real-world templates.
Two rendering contexts
Before diving into patterns, it's important to understand where Nunjucks expressions run and what variables are available:
| Context | When | Variable scope | Example |
|---|---|---|---|
fetch:template (Legacy) | At creation time, by the Witboost scaffolder | values.* (from template.yaml steps) | ${{ values.name }} |
%SKELETON rendering (Skeleton) | At every read, by the Witboost runtime | parameters.* (from parameters.yaml) + values.* for static fields + env.* for environment-specific values | ${{ parameters.name }} |
All examples in this guide show the values.* namespace (Legacy / fetch:template context). Replace values. with parameters. when writing %SKELETON catalog-info files for Skeleton Entity templates.
Variable declarations
Use {% set %} to compute a derived string once and reuse it multiple times in the file. This is the standard pattern to normalize domain/dataproduct/component names into URL-safe strings:
{% set domainNameNormalized = values.domain | replace(r/domain:| |-/, "") %}
{% set dataProductNameNormalized = values.dataproduct.split(".")[1] | replace(r/ |-/g, "") %}
{% set dataProductMajorVersion = values.identifier.split(".")[2] %}
{% set componentNameNormalized = values.name.split(" ") | join("") | lower %}
Then reuse throughout the file without repeating the filter chain:
annotations:
gitlab.com/project-slug: 'my-company/witboost/${{ domainNameNormalized }}/${{ dataProductNameNormalized }}/${{ componentNameNormalized }}'
mesh:
fullyQualifiedName: ${{ values.domainName }} - ${{ values.dataproductName }} - version ${{ dataProductMajorVersion }} - ${{ values.name }}
String normalization filters
These filters are chained with | and are applied left-to-right:
| Pattern | Description | Input | Output |
|---|---|---|---|
| replace(r/domain:| |-/, "") | Strip domain:, spaces, hyphens | domain:marketing finance | marketingfinance |
| replace(r/ |-/g, "") | Strip spaces and hyphens globally | my-data product | mydataproduct |
split(".")[1] | Extract segment by dot | ns.my-dp.1 | my-dp |
split(" ") | join("") | lower | Word-join then lowercase | Sales Output Port | salesoutputport |
| upper | Uppercase all | hello | HELLO |
| lower | Lowercase all | HELLO | hello |
The r/pattern/flags syntax is JavaScript-style regex supported by the Nunjucks replace filter in the Witboost environment. Use g flag for global (replace all occurrences), omit it to replace only the first.
String concatenation
Use + to concatenate strings and variables:
version: ${{ dataProductMajorVersion + ".0.0" }}
or simply put the strings one after the other:
gitUrl: https://gitlab.com/my-company/witboost/${{ domainNameNormalized }}/${{ dataProductNameNormalized }}/${{ componentNameNormalized }}.git
Conditional rendering
The {% if %}...{% endif %} block conditionally emits content. The most common pattern tests list length:
readsFrom: {% if values.readsFrom | length > 0 %}{% for i in values.readsFrom %}
- ${{ i }}
{% endfor %}{% else %}[]{% endif %}
For a field that can be absent or empty:
description: {% if values.description %}${{ values.description }}{% else %}''{% endif %}
Multi-branch example:
lifecycle: {% if values.maturity == "production" %}production{% elif values.maturity == "staging" %}quality-assurance{% else %}experimental{% endif %}
Loops
{% for %} iterates over arrays. Each loop variable is accessible using the loop variable name:
# catalog-info.yaml
tags: {% if values.tags | length > 0 %}{% for tag in values.tags %}
- tagFQN: ${{ tag }}
source: Glossary
labelType: Manual
state: Confirmed
{% endfor %}{% else %}[]{% endif %}
# Rendered descriptor with input
# values:
# tags: [ "PII", "Confidential", "Privileged" ]
tags:
- tagFQN: PII
source: Glossary
labelType: Manual
state: Confirmed
- tagFQN: Confidential
source: Glossary
labelType: Manual
state: Confirmed
- tagFQN: Privileged
source: Glossary
labelType: Manual
state: Confirmed
Accessing object properties in the loop variable:
# catalog-info.yaml
businessConcepts: {% if values.businessTerms | length > 0 %}{% for businessTerm in values.businessTerms %}
- tagFQN: ${{ businessTerm.value }}
source: Glossary
labelType: Manual
state: Confirmed
{% endfor %}{% else %}[]{% endif %}
# Rendered descriptor with input
# values:
# businessTerms:
# - value: PII
# origin: http://example.com
# - value: Confidential
# origin: http://example.com
# - value: Privileged
# origin: http://example.com
tags:
- tagFQN: PII
source: Glossary
labelType: Manual
state: Confirmed
- tagFQN: Confidential
source: Glossary
labelType: Manual
state: Confirmed
- tagFQN: Privileged
source: Glossary
labelType: Manual
state: Confirmed
Nested loops
When schema columns include per-column tags (themselves arrays), the pattern nests two loops:
schema: {% if values.schemaColumns | length > 0 %}
{% for col in values.schemaColumns %}
- name: ${{ col.name }}
dataType: ${{ col.dataType }}
tags: {% if col.tags | length > 0 %}{% for tag in col.tags %}
- tagFQN: ${{ tag }}
source: Glossary
labelType: Manual
state: Confirmed
{% endfor %}{% else %}[]{% endif %}
{% endfor %}
{% else %}[]{% endif %}
Including a special boolean flag alongside a loop (e.g., a PII checkbox):
tags: {% if col.tags | length > 0 or col.piiCheck %}
{% if col.tags | length > 0 %}
{% for tag in col.tags %}
- tagFQN: ${{ tag }}
source: Glossary
labelType: Manual
state: Confirmed
{% endfor %}
{% endif %}
{% if col.piiCheck %}
- tagFQN: PII
source: Tag
labelType: Manual
state: Confirmed
{% endif %}
{%- else %}[]{% endif %}
Safe YAML quoting with | dump
Use | dump on string fields whose values might contain special YAML characters (colons, quotes, newlines). | dump serializes the value as a JSON string literal: always double-quoted and with special characters escaped:
description: ${{ values.description | dump }}
name: ${{ values.name | dump }}
Without | dump, a description like "This is: a test" might produce malformed YAML. With | dump it becomes "This is: a test" (JSON-escaped).
Default values
Use | default(fallback) to provide a fallback when a value is undefined, null, or empty string:
confidentiality: ${{ values.confidentiality | default("public") }}
To also fall back when the value is false (in addition to undefined/null), pass true as the second argument:
enabled: ${{ values.featureEnabled | default(false, true) }}
Skeleton Entity scope: parameters.* and env.*
For Skeleton Entities you can access directly the parameters.* for user-supplied values from parameters.yaml and env.* for environment specific variables:
%SKELETON
{% set domainNameNormalized = parameters.domain | replace(r/domain:| |-/, "") %}
{% set dataProductMajorVersion = parameters.identifier.split(".")[2] %}
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ parameters.identifier }}
Environment-specific values defined under environmentParameters in parameters.yaml are accessible via env.*:
# skeleton/parameters.yaml
environmentParameters:
development:
database: ${{ parameters.databaseName }}_dev
production:
database: ${{ parameters.databaseName }}_prod
# catalog-info.yaml (%SKELETON)
specific:
database: ${{ env.database }}
env.* automatically resolves to the correct environment's value depending on which deployment environment Witboost is evaluating the component for. These values can be manually defined on the skeleton/parameters.yaml file as shown above, or be the output of user-input values from a form step with environmentParameters: true:
spec:
parameters:
# ..
- title: Per-environment database definition
environmentParameters: true
description: Define the target database for each environment
required:
- database
properties:
database:
title: Database
description: Target database for table creation, one for each configured environment
type: string
For more information about Environment Parameters check out this section.