Skip to content

Template Functions, Filters & Metadata

VIBE extends the standard Jinja environment with built-in functions, filters, and metadata that are available in every template. You can also add your own via custom extensions.

Built-in Functions

VIBE provides these functions you can call directly in your templates.

  • defined('variable_id'): Checks if a variable has been answered. This is more powerful than a simple {% if variable %} because it doesn't fail if the variable doesn't exist in the context yet. It always returns true or false.

    {% if defined('optional_notes') %}
    **Additional Notes:**
    {{ optional_notes }}
    {% endif %}
    
  • show_message(message_id): Triggers the display of a note, warning, or error block from your config.yml. This function doesn't print anything itself; it just tells VIBE to make the message block relevant.

    {% if total_value > 100000 %}
      {{ show_message(large_contract_warning) }}
    {% endif %}
    
  • insert(component_id): Inserts a component into the template at that point. See Reuse with Components for details.

    {{ insert('contract_header') }}
    
  • appendix(component_id, alias): Creates a separate appendix document that will be generated alongside your main document. See Generating Appendices and Annexes for details.

    {{ appendix('schedule_a', 'payment_schedule') }}
    
  • recommend(template_id, label=None, **mappings): Registers a linked follow-up interview. It renders nothing in the document output, but adds a "Continue" link in the interview UI when relevant. See Linked Interviews for full behavior and mapping examples.

    {{ recommend('dpa', "Data Processing Agreement", party_info=kund_info) }}
    
  • kb_link(page, text): Generates a link to a knowledge base article. Used in help text in config.yml to give users access to longer explanations that open in a new tab. The articles are Markdown files stored in a kb/ subdirectory inside the template bundle.

    Place your articles in the template's kb/ directory:

    templates/my-template/
    ├── config.yml
    ├── template.md
    └── kb/
        ├── pricing.md
        └── legal/
            └── gdpr.md
    

    Then reference them from a question's help field:

    questions:
      pricing_model:
        label: Pricing model
        type: select
        help: >
          Choose the pricing model for this engagement.
          {{ kb_link("pricing", "Read more about pricing models") }}
        options:
          - Fixed price
          - Time and materials
    

    The first argument is the page name (without .md), which can include subdirectories (e.g. "legal/gdpr"). The second argument is the link text. Articles are rendered as standalone HTML pages using the Markdown extra, sane_lists, and smarty extensions. The function works with both filesystem and Git-backed templates.

Built-in Filters

Filters modify a variable's value before it's printed, using the pipe | character.

  • | or('placeholder text'): Shows a styled placeholder when a variable is unanswered, and passes the value through unchanged when it has been answered. In the web preview the placeholder appears with brackets and a yellow highlight. In DOCX output it returns rich inline content, so in Word templates you must print it with {{r ... }} rather than plain {{ ... }}. Locale aliases: Swedish eller, German oder, French ou, Spanish o.

    Prepared for: {{ client_name | or('Client Name') }}
    

    In a Word template:

    Prepared for: {{r client_name | or('Client Name') }}
    

    For Swedish templates:

    Avtal med: {{ kundnamn | eller('Ange kundnamn') }}
    
  • | safe_default('fallback text'): Returns plain fallback text (no placeholder styling) when a variable is unanswered. Prefer | or for most use cases — it produces visually distinct placeholders. Use safe_default only when you need a raw text substitution without styling, for example inside expressions or conditionals where HTML markup would be inappropriate.

    {{ client_name | safe_default('unnamed client') }}
    
  • | selected_choices: Used with multichoice variables (a question type that allows multiple selections — see Question Types Reference). It returns a list of only the options the user actually checked.

    {% for item in service_scope | selected_choices %}
    - {{ item }}
    {% endfor %}
    
  • | markdown: Converts a string containing Markdown into formatted output. In Markdown/web contexts it produces HTML. In DOCX output it is converted into a DOCX subdocument, so in Word templates you must print it with {{p ... }} rather than plain {{ ... }}.

    {{ project_description | markdown }}
    

    In a Word template:

    {{p project_description | markdown }}
    

For locale-aware text transformations (| plural, | doublet, etc.), see Linguistic Features.

Formatting Filters for Special Types

Some question types (amount, period) produce structured objects rather than simple values. When you print them directly with {{ my_amount }} or {{ my_period }}, VIBE formats them automatically. You can also use explicit filters for more control:

  • | format_number: Formats a numeric value with locale-aware decimal and grouping separators. Trailing fractional zeros are stripped automatically: 2.0 becomes 2, 2.50 becomes 2,5 (in Swedish locale).

    Avtalet ska gälla under {{ years|format_number }} år
    

    Produces output like 1,5 år or 2 år (Swedish) or 1.5 years (English). Particularly useful for number-type questions where raw float output like 2.0 or 2.500 would look wrong in the document.

  • | format_amount: Formats an amount value with its currency symbol. Handles locale-specific formatting (e.g., Swedish Krona as 1 000 kr).

    {{ project_budget | format_amount }}
    

    Produces output like $1,234.56 or €500.00 depending on the currency.

  • | format_period_display: Formats a period value as readable text.

    {{ contract_duration | format_period_display }}
    

    Produces output like 3 months, until December 31, 2025, indefinitely, or ends on project completion depending on the period mode. Automatically handles singular/plural and locale-aware date formatting.

  • | period_unit_name: Converts a period unit string (like "days" or "months") to its localized display name. Primarily useful when you need to format period parts individually.

    {{ contract_duration.unit | period_unit_name }}
    

Note: In most cases, printing the variable directly ({{ project_budget }} or {{ contract_duration }}) produces the same result as applying the filter explicitly. The filters are useful when you need to format a value that isn't the direct output of a question, such as a computed amount or a period extracted from a more complex structure.

Template Metadata

VIBE provides a special meta object that gives you access to session information, timestamps, and template details.

  • meta.session_id: The unique identifier for this interview session.
  • meta.now(): Current timestamp (use with |strftime('%Y-%m-%d') to format).
  • meta.template.name: The template identifier.
  • meta.template.version: The version (Git SHA or "WORKING_TREE").
  • meta.assistant.<name>.turns: Number of interactions with an AI assistant (Assistant extension only).
  • meta.assistant.<name>.tokens: Total tokens consumed by an AI assistant (Assistant extension only).
Generated {{ meta.now()|strftime('%B %d, %Y') }} | Session: {{ meta.session_id }}

See Template Metadata Reference for complete details.


Custom Extensions

Template extensions let you extend the Jinja rendering environment with custom Python functions and filters. This is useful for domain-specific logic such as compatibility shims, conditional formatting that adapts to the output format (Markdown vs DOCX), or accessing external state like user identity.

Convention

File Effect
templatefunctions.py Exported names become Jinja globals (callable as {{ my_func() }})
templatefilters.py Exported names become Jinja filters (usable as {{ value \| my_filter }})

Both files are optional. When present, they are loaded after computations.py (if any), so template extensions can build on computed values.

Controlling Exports

If the module defines __all__, only those names are exported:

# templatefunctions.py
__all__ = ["user_info"]

def user_info():
    """Only this function is exported."""
    ...

def _internal_helper():
    """Not exported (underscore prefix would exclude it anyway)."""
    ...

Without __all__, every public (non-underscore) callable is exported automatically.

The ctx Proxy

The framework injects a ctx proxy into each loaded module. Template authors reference ctx as a module-level global — no imports, closures, or factories needed:

# templatefunctions.py
def include_docx_template(templatefile, **kwargs):
    """Return a subdoc during DOCX rendering, placeholder during probing."""
    if ctx.get("_docxtpl"):
        subdoc = ctx.get("_subdoc")
        if subdoc is not None:
            return subdoc
    return f"[include_docx_template: {templatefile}]"

ctx behaves like the interview's NestedValue context:

Access pattern Example
Attribute access ctx.field_name
Item access ctx["field_name"]
in check "field_name" in ctx
.get() with default ctx.get("field_name", "fallback")

Each HTTP request gets its own isolated context value, making ctx thread-safe via contextvars.ContextVar.

Internal context keys

During different rendering phases, the framework sets internal keys on the context that extensions can inspect:

Key Present when Value
_is_probing Probing phase True
_is_web_preview Live preview rendering True
_docxtpl DOCX final rendering The DocxTemplate instance
_subdoc DOCX wrapper rendering The subdoc object

Configuring Custom Filenames

By default, VIBE looks for templatefunctions.py and templatefilters.py. You can override these names in config.yml:

# config.yml
template_functions: myfuncs.py
template_filters: myfilts.py

This is useful when a template directory uses a different naming convention or shares extension files across templates.

Reserved Names

Exported names must not collide with VIBE built-in Jinja globals or filters. Collisions are logged as errors and the colliding name is skipped (other names from the same module still load normally).

Reserved globals: insert, defined, show_message, recommend, kb_link, appendix, _, render_list_structured_field, _appendices

Reserved filters: markdown, or, safe_default, selected_choices, format_number, format_amount, period_unit_name, format_period_display, plural, definite, possessive, itemize, cardinal, ordinal, doublet, roman, alpha, indefinite, format_datetime

Scope

Template extensions are available during:

  • Probing — the mechanism that determines which questions are relevant
  • Web preview — the live preview shown during the interview
  • Final rendering — both Markdown and DOCX output paths
  • DOCX wrapper rendering — extensions are copied to the wrapper environment

Examples

Filter: graceful fallback display

# templatefilters.py
from docxtpl import RichText

def eller(value, placeholder):
    """Return value when truthy, or a highlighted placeholder when falsy."""
    if value:
        return str(value) if not isinstance(value, RichText) else value

    if ctx.get("_docxtpl"):
        rt = RichText()
        rt.add(f"[{placeholder}]", highlight="yellow")
        return rt
    return f"[{placeholder}]"

Usage in template: {{ organization_name | eller("ange organisation") }}

Function: user identity

# templatefunctions.py
from types import SimpleNamespace
from flask import session

def user_info():
    """Return user identity from the Flask session."""
    full_name = session.get("user_name", "")
    parts = full_name.split(None, 1)
    return SimpleNamespace(
        first_name=parts[0] if parts else "",
        last_name=parts[1] if len(parts) > 1 else "",
        email=session.get("email", ""),
    )

Relationship to computations.py

Both computations.py and template extensions add callables to the Jinja environment. The key differences:

Aspect computations.py Template extensions
Loading order First After computations
ctx proxy Not available Injected automatically
Collision checking No Yes (against reserved names)
Filters No (globals only) Yes (templatefilters.py)
__all__ support No Yes