Skip to main content

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:

ContextWhenVariable scopeExample
fetch:template (Legacy)At creation time, by the Witboost scaffoldervalues.* (from template.yaml steps)${{ values.name }}
%SKELETON rendering (Skeleton)At every read, by the Witboost runtimeparameters.* (from parameters.yaml) + values.* for static fields + env.* for environment-specific values${{ parameters.name }}
info

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 = parseEntityRef(values.domain).name | slugify("") %}
{% set dataProductNameNormalized = parseWitboostId(values.dataproduct).systemName | slugify("") %}
{% set dataProductMajorVersion = values.identifier.split(".")[2] %}
{% set componentNameNormalized = slugify(values.name, "") %}
info

domain and dataproduct are entity-ref values (e.g. domain:finance, system:default/finance.transactions.0), so parseEntityRef/parseWitboostId extract their plain name/system segment before slugify cleans up the result. values.identifier (e.g. finance.transactions.0) is instead a plain, dot-joined Builder identifier with no entity-ref prefix, so split(".")[N] remains the right way to extract its segments. See Custom functions and Nunjucks Helpers for the full reference.

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:

PatternDescriptionInputOutput
| replace(r/domain:| |-/, "")Strip domain:, spaces, hyphensdomain:marketing financemarketingfinance
| replace(r/ |-/g, "")Strip spaces and hyphens globallymy-data productmydataproduct
split(".")[1]Extract segment by dotns.my-dp.1my-dp
split(" ") | join("") | lowerWord-join then lowercaseSales Output Portsalesoutputport
| upperUppercase allhelloHELLO
| lowerLowercase allHELLOhello

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.

tip

For entity-ref-shaped values (domain, dataproduct/parentRef), prefer parseEntityRef/parseWitboostId combined with slugify over the first two rows of this table: they correctly strip the entity-ref prefix instead of relying on it happening to be the first regex match, and slugify also lowercases and strips diacritics. See Variable declarations above and Custom functions for details.

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.