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 = 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:

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.

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.