Configuration File Reference

This guide provides a comprehensive reference for all the configuration files used in VIBE. Understanding these files is key to building, customizing, and managing both the system and individual templates.

Table of Contents

  1. System Configuration (config.yml)
    • Core application and web server settings.
  2. Template Configuration (config.yml)
    • Defines the questions, logic, and metadata for a single interview template.
  3. Component Configuration (component.yml)
    • Defines the interface and logic for a reusable component.

System Configuration (config.yml)

This file is located in the root directory of the VIBE application. It controls the overall behavior of the system, including server settings, session management, and paths to template/component source directories.

Key Type Required Description
secret_key string Yes A long, random string used for cryptographic signing, such as session cookies.
session_storage_key string Yes A long, random string used to sign saved interview states (.vibestate files). Auto-generated if not present.
template_sources mapping Yes Maps a unique template_id to its source directory path.
component_sources list No A list of paths to directories containing reusable components.
llm_endpoints mapping No Assistant extension: Central registry of available Large Language Models.
assistant_logging_enabled boolean No Assistant extension: Enables writing assistant request/response/SSE logs to the data directory. Defaults to true. Set to false to disable assistant logging.
embedding_providers mapping No Review extension: Defines embedding models for similarity search. See Embedding Providers.
rerank_providers mapping No Review extension: Defines reranking models for search refinement. See Rerank Providers.
review_backends mapping No Review extension: Configures OCR and other document processing backends. See Review Backends.
dev_settings mapping No Development-time settings used when running in debug mode without a PASETO token. Contains user info, session_context data, and grants. See Development Settings and Session Context for details.
template_validation boolean No If false, disables the initial validation run on startup. Defaults to true.
session_file_system_dir string No Path to the directory for storing session files. Defaults to ./.flask_session.
mount_path string No Optional URL path prefix where the application should be mounted. When set, all routes and static files are served under this prefix. For example, setting mount_path: "vibe" mounts the app at /vibe/ so the main page is at http://localhost:5001/vibe/. Leave empty (default) to mount at the root path.
locale string No Sets the default locale for the entire application (e.g., 'en', 'sv'). Affects translations, date/number/currency formatting. Can be overridden by template-level config.
# Example system config.yml

# --- Security ---
secret_key: a-very-long-and-random-string-for-sessions
session_storage_key: another-super-secret-key-for-state-files

# --- Application Mounting (optional) ---
# Leave empty or omit to mount at root path
# Set to a path like "vibe" to mount at http://localhost:5001/vibe/
mount_path: ""

# --- Template and Component Paths ---
template_sources:
  nda_template:
    path: ./templates/nda
  service_agreement:
    path: ./templates/sa

component_sources:
  - ./components/shared
  - ./components/legal_clauses

# --- LLM Configuration (for Administrators) ---
llm_endpoints:
  default:
    provider: vibe.llm_providers.openai.OpenAIProvider
    config:
      api_key: "{{ env('OPENAI_API_KEY') }}"
      model: gpt-4-turbo-preview

# --- Assistant Logging (optional) ---
assistant_logging_enabled: true

# --- Embedding and Rerank Providers (for VIBE Review) ---
embedding_providers:
  local:
    provider: vibe.embedding_providers.local.LocalEmbeddingProvider
    config:
      model: multilingual-large

rerank_providers:
  local:
    provider: vibe.rerank_providers.local.LocalRerankProvider
    config: {}

# --- Review Backends (for VIBE Review scanned PDF support) ---
review_backends:
  ocr:
    backend: tesseract_docker
    text_layer_min_chars: 10
    tesseract:
      oem: 1
      psm: 4
      user_words: null
  pdf:
    backend: pdfplumber
  docx:
    backend: libreoffice_docker

# --- Development Settings ---
# Used when running in debug mode without a PASETO token.
# In production, this data comes from the authentication system.
dev_settings:
  user:
    name: Development User
    email: dev@example.com
  session_context:
    organization:
      name: Development Corp
      org_number: "123456-7890"
    settings:
      default_currency: USD
  grants: []

# --- Internationalization ---
locale: en

LLM Endpoint Configuration (llm_endpoints)

This section is for System Administrators and applies to the Assistant extension. It provides a central, secure registry of Large Language Model (LLM) services that can be used by AI Drafting Assistants in templates. By defining endpoints here, you control which models are available, manage API keys securely, and can enforce cost and governance policies.

Template authors can then select from these predefined endpoints by referencing their key (e.g., claude-opus) in their template's assistants configuration.

Each entry in the llm_endpoints mapping consists of a unique alias and two keys: provider and config.

  • Alias (e.g., default, claude-opus): A unique, friendly name for the endpoint. Template authors will use this name. It is recommended to always have an endpoint named default.
  • provider: The full Python path to the VIBE LLM Provider class that handles communication with the specific AI service.
  • config: A dictionary of parameters passed directly to the provider. This is where you set API keys, model names, and other service-specific options.

Available Providers:

  • vibe.llm_providers.openai.OpenAIProvider: For OpenAI models (e.g., GPT-5) and any OpenAI-compatible API endpoint.
  • vibe.llm_providers.anthropic.AnthropicProvider: For Anthropic's Claude models.
  • vibe.llm_providers.gemini.GeminiProvider: For Google's Gemini models.
  • vibe.llm_providers.mistral.MistralProvider: For Mistral AI's models.
  • vibe.llm_providers.ollama. OllamaProvider: For connecting to a local Ollama instance.
  • vibe.llm_providers.mock.ConfigurableMockProvider: A simple provider for development and testing that returns a fixed response.

Security Note: API keys should always be loaded from environment variables using the env() function for security, as shown in the example below.

# Detailed llm_endpoints example in system config.yml

llm_endpoints:
  # The default model used when a template author does not specify one.
  default:
    provider: vibe.llm_providers.openai.OpenAIProvider
    config:
      # Securely load the API key from an environment variable.
      api_key: "{{ env('OPENAI_API_KEY') }}"
      model: gpt-4-turbo-preview

  # An alias for a more powerful, potentially more expensive model.
  claude-opus:
    provider: vibe.llm_providers.anthropic.AnthropicProvider
    config:
      api_key: "{{ env('ANTHROPIC_CLAUDE_API_KEY') }}"
      model: claude-3-opus-20240229

  # An endpoint for a local model served via Ollama.
  local-llama:
    provider: vibe.llm_providers.ollama.OllamaProvider
    config:
      # No API key needed for local Ollama.
      model: llama3
      base_url: http://localhost:11434

  # The mock provider for development and testing.
  mock_drafter:
    provider: vibe.llm_providers.mock.MockProvider
    config: {}

Configurable Mock Provider (vibe.llm_providers.mock.ConfigurableMockProvider)

Use this provider when you need a deterministic assistant workflow (for demos, documentation, or local QA). Each entry in config.responses represents one AssistantMessage. Responds are expressed as a list:

  • null means the assistant emits an empty message (no tool calls).
  • A nested list describes the tool calls for that turn. Each tool can be defined as either:
  • A scalar tool name (- finalize)
  • A mapping from tool name to arguments (- update_chat: {content: ...})
  • The provider auto-generates tool-call IDs and follows the order of the list, matching the behavior of COMPLETE_WORKFLOW_RESPONSES (see tests/assistant/integration/browser.py for the equivalent Python fixture).

You can also control streaming behavior:

  • chunk_size (characters) splits tool arguments/text when streaming tool calls.
  • chunk_delay (milliseconds) pauses between each chunk when chunk_size > 0.
llm_endpoints:
  demo_configurable_mock:
    label: "Demo provider"
    provider: vibe.llm_providers.mock.ConfigurableMockProvider
    config:
      chunk_size: 5
      chunk_delay: 80
      responses:
        - null
        - - update_chat:
              content: Draft introduction placeholder
        - - finalize
        - null
        - - insert_blocks:
              position:
                at: end
              blocks:
                - id: intro
                  content: |
                    # Project Goals

                    This document will outline the goals and objectives for the project.
              reason: Creating initial project structure

OpenAI Provider (vibe.llm_providers.openai.OpenAIProvider)

Use this provider for OpenAI-hosted models (gpt-5-*, gpt-4.1*, o4-*, gpt-oss-*) and other services that expose the OpenAI Responses API. Set the following keys inside the endpoint’s config block:

  • api_key (required) – API credential; load it via {{ env('OPENAI_API_KEY') }} or a similar environment binding.
  • model (required) – Model identifier. The provider automatically selects the Responses API path for the latest OpenAI families listed above.
  • base_url – Override the API host for OpenAI-compatible vendors (e.g. Berget). Defaults to https://api.openai.com/v1.
  • timeout – Request timeout in seconds (default 60).
  • tools – Enable tool calling and streaming tool arguments (default true). Set false for plain text interactions.
  • remove_empty_content – Controls handling of empty content in assistant messages with tool calls. When true (default), omits the content field entirely for OpenAI/Mistral compatibility. When false, includes content: null for third-party providers that require the field to be present. Only relevant when using tools.
  • max_tokens – Upper bound for generated tokens (max_output_tokens when using Responses models).
  • temperature – Sampling temperature. Forwarded only when tools are disabled; omit it for Responses-first models where reasoning.effort is preferred.
  • reasoning_effort – Sets reasoning.effort (low, medium, or high) on Responses requests. Defaults to medium when omitted.
  • text_verbosity – Sets text.verbosity (low, medium, or high) for Responses models. Defaults to medium and must match an allowed value.
  • playback_from_file / playback_session_id – Optional debug settings that replay logged provider output during local troubleshooting.

reasoning_effort and text_verbosity are ignored for legacy chat-completions models; they only apply when the provider opts into the Responses API pathway.

Example: Third-party provider requiring content: null

llm_endpoints:
  third_party_provider:
    provider: vibe.llm_providers.openai.OpenAIProvider
    config:
      api_key: "{{ env('THIRD_PARTY_API_KEY') }}"
      model: third-party-model-name
      base_url: https://api.thirdparty.com/v1
      remove_empty_content: false  # This provider requires content: null in tool call messages

Embedding Provider Configuration (embedding_providers)

This section defines embedding models used for similarity search in VIBE Review. Embeddings convert text into vector representations for similarity matching.

Each entry in the embedding_providers mapping consists of a unique alias and two keys: provider and config.

  • Alias (e.g., local, berget): A unique name for the provider. Review templates reference this in their review.embedding setting.
  • provider: The full Python path to the embedding provider class.
  • config: A dictionary of parameters passed to the provider.

Available Providers:

  • vibe.embedding_providers.local.LocalEmbeddingProvider: Local inference using fastembed (multilingual by default). No API key required.
  • vibe.embedding_providers.berget.BergetEmbeddingProvider: Cloud API using intfloat/multilingual-e5-large model.
  • vibe.embedding_providers.mock.ConfigurableMockEmbeddingProvider: For testing.
# Embedding providers in system config.yml

embedding_providers:
  # Local inference (default) - no API key needed
  local:
    provider: vibe.embedding_providers.local.LocalEmbeddingProvider
    config:
      model: multilingual-large  # or: e5-large, multilingual-small

  # Cloud API
  berget:
    provider: vibe.embedding_providers.berget.BergetEmbeddingProvider
    config:
      api_key: "{{ env('BERGET_API_KEY') }}"

Rerank Provider Configuration (rerank_providers)

This section defines reranking models used to refine search results in VIBE Review. Rerankers use cross-encoder models to score document-query relevance more accurately than embeddings alone.

Each entry in the rerank_providers mapping consists of a unique alias and two keys: provider and config.

  • Alias (e.g., local, berget): A unique name for the provider. Review templates reference this in their review.reranking setting.
  • provider: The full Python path to the rerank provider class.
  • config: A dictionary of parameters passed to the provider.

Available Providers:

  • vibe.rerank_providers.local.LocalRerankProvider: Local inference using sentence-transformers cross-encoder. No API key required.
  • vibe.rerank_providers.berget.BergetRerankProvider: Cloud API reranking service.
  • vibe.rerank_providers.mock.ConfigurableMockRerankProvider: For testing.
# Rerank providers in system config.yml

rerank_providers:
  # Local inference (default) - no API key needed
  local:
    provider: vibe.rerank_providers.local.LocalRerankProvider
    config:
      model: cross-encoder/ms-marco-MiniLM-L-6-v2

  # Cloud API
  berget:
    provider: vibe.rerank_providers.berget.BergetRerankProvider
    config:
      api_key: "{{ env('BERGET_API_KEY') }}"

Using Providers in Review Templates

Extension note: This section applies only when the Review extension is enabled.

Review templates reference these providers by their alias in the review configuration block:

# In a review template's config.yml

interview_mode: review

review:
  embedding: local      # Key from embedding_providers
  reranking: berget     # Key from rerank_providers
  evaluation: claude    # Key from llm_endpoints

If not specified, templates default to local for embedding and reranking.

Review Backends Configuration (review_backends)

Extension note: Review backends are only used by the Review extension.

The review_backends section configures document processing backends for VIBE Review. This includes OCR (Optical Character Recognition) for processing scanned PDFs and DOCX conversion for viewing Word documents.

Structure:

Key Type Default Description
ocr mapping {} OCR backend configuration for scanned PDF processing.
ocr.backend string tesseract_docker OCR backend type. Set to none to disable OCR.
ocr.text_layer_min_chars integer 10 Minimum non-whitespace characters required to treat a page as text-layer. Pages below this threshold and dominated by a single large image (>80% of page area) are OCR'd.
ocr.tesseract mapping {} Tesseract-specific configuration overrides.
ocr.tesseract.oem integer 1 Tesseract OCR Engine Mode. Typical values: 0 (legacy), 1 (LSTM), 2 (legacy+LSTM), 3 (default).
ocr.tesseract.psm integer 4 Tesseract Page Segmentation Mode. Typical values: 4 (single column), 6 (single block), 11 (sparse text).
ocr.tesseract.user_words string or mapping null Optional wordlist file path (single list) or a mapping of language codes to file paths. Paths are resolved relative to the current working directory.
docx mapping {} DOCX to PDF conversion backend configuration for viewing DOCX files.
docx.backend string libreoffice_docker DOCX converter backend type. libreoffice_docker_volume uses a shared volume mount. Set to none to disable DOCX viewing.
pdf mapping {} PDF text extraction backend configuration.
pdf.backend string pdfplumber PDF text extraction backend. pymupdf is faster but may produce different text extraction results.

Example (showing defaults explicitly):

review_backends:
  ocr:
    backend: tesseract_docker
    text_layer_min_chars: 10
    tesseract:
      oem: 1
      psm: 4
      user_words: null
  pdf:
    backend: pdfplumber
  docx:
    backend: libreoffice_docker

Prerequisites for tesseract_docker backend:

  1. Docker must be installed and running
  2. Tesseract container must be running:
    docker compose up -d tesseract
    

When a scanned PDF is uploaded without OCR configured, VIBE Review will display an actionable error message explaining how to enable OCR support.

Prerequisites for libreoffice_docker backend:

  1. Docker must be installed and running
  2. LibreOffice container must be running:
    docker compose up -d libreoffice
    

The libreoffice_docker_volume variant uses a shared volume mount instead of docker cp for file transfer, which can be more reliable in some environments (e.g., WSL with Docker Desktop).

Development Settings (dev_settings)

The dev_settings section provides a convenient way to simulate PASETO token data during local development. When running in debug mode (debug: true) and no PASETO token is present, VIBE uses these settings to populate the session.

Structure:

Key Type Description
user mapping User information that would normally come from the authentication system.
user.name string The user's display name.
user.email string The user's email address.
session_context mapping Pre-filled data that templates can declare as requirements. This is the development equivalent of the vibe_config payload in a PASETO token.
grants list A list of permission grants (empty list for most development scenarios).

How it works:

  1. When a user starts an interview, VIBE checks for a PASETO token first.
  2. If no token is found and debug: true is set, VIBE uses dev_settings instead.
  3. If neither is available, the session starts with an empty context (which is fine as long as the template doesn't require session context).

Session Context Validation:

If a template declares session_context requirements (using definition-only components), VIBE validates the dev_settings.session_context against those requirements at session start. Missing or incorrectly typed values will result in a 400 error.

# Example dev_settings configuration
dev_settings:
  user:
    name: Fred Bloggs
    email: fred@initrode.se
  session_context:
    organization:
      name: Initrode AB
      org_number: "556123-4567"
    ramavtal:
      svarsfrist:
        antal: 10
        enhet: arbetsdagar
  grants: []

Production Note: In production, dev_settings is ignored entirely. All user information and session context must come from a valid PASETO token provided by your authentication system.


Template Configuration (config.yml)

This file is located inside each template's source directory (as defined in TEMPLATE_SOURCES). It is the brain of the interview, defining all questions, reusable data structures, and computed logic.

Key Type Required Description
title string No A human-readable title for the template, shown on the main listing page.
description string No A brief description of the template's purpose.
template_file string No The name of the main document file (e.g., main.docx). Defaults to template.md or template.docx.
interview_mode string No Controls the interview UI mode. Options: standard (traditional forms with live preview, default), assistant (Assistant extension), review (Review extension).
session_context list No Declares external data requirements using definition-only components. See Session Context Requirements below.
assistants mapping No Assistant extension: Defines AI Drafting Assistants available in this template.
questions mapping Yes The master list of all possible questions for the interview. See Question Types Reference for available types including text, number, bool, tristate, date, amount, enum, multichoice, list, structured, computable, and assisted (Assistant extension).
pages mapping No Divides questions into logical pages for long interviews. Each page (keyed by page ID) has title and questions (question definitions). See Paged Interviews.
definitions mapping No Defines reusable data structures that can be used in structured and list questions. See Reusable Data with definitions.
computables mapping No Defines variables whose values are calculated from other answers. See Automated Calculations with Computable Variables.
component_defaults mapping No Provides default values for component inputs, reducing repetition in insert() calls. See Building with Components.
locale string No Overrides the system-wide locale for this specific template.
imports mapping No Adds custom functions or global variables to the template's Jinja environment.
filters mapping No Adds custom filters to the template's Jinja environment.
ui mapping No Customizes the interview UI (template overrides, stylesheet, preview, layout). See Customizing the Interview UI.
_insert_questions_* null No A special marker key to insert a component's questions into the main question order at this position. The * should be the component's alias.
# Example template config.yml in templates/my_template/

title: Service Agreement Template
description: Generates a standard service agreement for new clients.
template_file: agreement.docx
locale: en_US

assistants:
  scope_drafter:
    label: "AI: Draft Scope of Work"
    model: default

imports:
  get_signer_title: my_helpers.get_title_for_signer

filters:
  euros: shared_filters.format_euros

component_defaults:
  company_name: My Awesome Company Inc.
  default_currency: USD

questions:
  client_name:
    type: text
    label: Client's Full Legal Name

  contract_value:
    type: number
    label: Total Contract Value

  _insert_questions_signature_block: null

  include_nda:
    type: bool
    label: Include a Non-Disclosure Agreement?
    default: false
    required: false

definitions:
  person_definition:
    first_name:
      type: text
      label: First Name
    last_name:
      type: text
      label: Last Name

UI Customization (ui block)

Use the optional ui block when you need to change how the interview itself looks (as opposed to how the generated document renders). All paths are relative to the template directory and are sandboxed so they cannot escape the bundle.

Key Type Required Description
templates string | null No Directory containing override templates/widgets. When provided, VIBE checks this folder before the built-ins. Set to null (or omit the key) to disable overrides entirely.
css string No Path to a CSS file that should be injected into the interview <head>. When present it is served at /interview/<template_id>/ui.css (respecting the selected version).
preview boolean No Show the live preview panel (default true). Set to false for a single-column interview.
layout string No Group layout for templates using groups:. Options: paged, accordion, tabs, flat. Defaults to paged when groups are present.

Example:

ui:
  templates: ui
  css: ui/theme.css

Put individual overrides under the configured directory using the same relative path as VIBE's built-in templates (for example ui/question_types/bool.html or ui/layout/fragments/site_header.html). The CSS file is optional; when configured it loads after VIBE's default stylesheet so you can safely override variables and component styles.

Session Context Requirements (uses)

Templates can declare that they require certain external data to be provided at session start. This data typically comes from a PASETO token (in production) or from dev_settings (during development).

Declaring Requirements:

The uses key is a list of references to definition-only components. Each item can be:

  • A simple string (component ID and alias are the same): - organization
  • A mapping for aliasing: - ramavtal: framework (component "ramavtal" is aliased as "framework" in the template)
# Template config.yml
uses:
  - organization           # Use component "organization" as "organization"
  - ramavtal: framework    # Use component "ramavtal" as "framework"

questions:
  contract_title:
    type: text
    label: Contract Title

Definition-Only Components:

A definition-only component provides data structure definitions without a template file. Create one by adding definition_only: true to its component.yml:

# components/organization/component.yml
definition_only: true

definitions:
  namn:
    type: text
    label: Organization Name
    required: true
  orgnr:
    type: text
    label: Organization Number
    required: true
  settings:
    type: structured
    fields:
      feature_enabled:
        type: bool
        required: false
        default: false

Using Session Context in Templates:

Once declared, session context values are available directly in your template:

# Contract for {{ organization.namn }}

Organization number: {{ organization.orgnr }}
{% if organization.settings.feature_enabled %}
Premium features are enabled.
{% endif %}

Validation:

At session start, VIBE validates that all required session context values are present and correctly typed. If validation fails, the user sees a 400 error explaining which values are missing or invalid.

Type Coercion:

Session context values from PASETO tokens are strings. VIBE automatically coerces them to the correct type based on the definition:

  • bool: "true"/"false" → True/False
  • number: "42" or "3.14" → int or float
  • text: Passed through as-is

Component Configuration (component.yml)

This file is located inside a component's directory. It defines a component's interface (inputs), its internal logic (questions, computables), and its relationship to other data structures (uses).


Component Defaults Configuration (defaults.yml)

This file is optionally located inside a file-drop component's directory (alongside the component template file). It provides a lightweight way to declare input expectations for simple file-drop components without requiring a full component.yml configuration.

Key Type Required Description
inputs mapping Yes Declares the data the file-drop component expects to receive. Each input follows the same field definition syntax as component.yml inputs.

Purpose and Usage:

  • Documentation: Clearly documents which variables a file-drop component expects from its host template
  • Validation: Enables VIBE to validate that required inputs are provided when the component is used
  • Component Defaults Integration: Inputs declared here can automatically receive values from the host template's component_defaults configuration
  • Optional Input Handling: Allows marking specific inputs as optional using required: false

Input Field Options: Each input in the inputs mapping can include:

  • required (boolean): Whether this input must be provided. Defaults to true.
  • type (string): The expected data type (text, number, bool, etc.). Optional but recommended for validation.
  • description (string): Human-readable description of the input's purpose.
# Example defaults.yml in components/signature_block/

inputs:
  # Required inputs that must be provided by host
  signer_name: 
    type: text
    description: Full name of the person signing the document

  signer_title:
    type: text
    description: Job title or role of the signer

  # Optional input with explicit marking
  signing_date:
    type: date
    required: false
    description: Date of signing (optional, can be filled later)

  # Input that will use component_defaults if available
  company_name:
    type: text
    description: Company name (will use component_defaults value if set)

Integration with Component Defaults: Inputs declared in defaults.yml automatically integrate with the host template's component_defaults. If a component default exists with a matching key, it will be used as the input value unless explicitly overridden in the insert() call.

Validation Behavior:

  • Required inputs (default) must either be provided in the insert() call or available through component defaults
  • Optional inputs (required: false) can be omitted without causing validation errors
  • Type validation is performed if a type is specified for the input

Key Type Required Description
definition_only boolean No When true, marks this as a definition-only component that provides data structure definitions for session context validation. These components don't require a template file.
label string No A human-readable label for the component. Especially important for appendices.
description string No A brief description of the component's purpose.
uses string No The ID of a definition from the host template. The component inherits all fields from this definition as its inputs. See Building with Components.
inputs mapping No Declares the data the component expects to receive. Each input is a field definition (like a mini-question).
questions mapping No Defines questions that are internal to this component and only become relevant when it is inserted.
computables mapping No Defines computed variables that are scoped to this component.
output string No If present, marks this component as an Appendix. The string is a Jinja template for the output filename. See Generating Appendices and Annexes.
encapsulation string No If set to strict, prevents the component from accessing host template variables that are not explicitly passed as inputs.
# Example component.yml in components/contact_card/

# This component links to a data structure defined in the host template
uses: person_definition

# It adds one additional input field of its own
inputs:
  department:
    type: text
    label: Department
    description: The contact's department, e.g., 'Sales'.
    required: false

# It also has its own internal question
questions:
  show_department:
    label: Show department on the contact card?
    type: bool
    default: true
    required: false
# Example appendix component.yml in components/pricing_schedule/

# Marks this as an appendix and defines the output filename pattern
output: "Appendix-{{ ref.numbering }}-{{ ref.label|slugify }}.docx"

label: Pricing Schedule
description: A detailed breakdown of pricing for all services.

# Defines questions that will appear in the UI when this appendix is included
questions:
  unit_price:
    type: number
    label: Price per unit
    min: 0

  currency:
    type: select
    label: Currency
    options:
      - USD
      - EUR
      - GBP
    default: USD