Skip to content

Template Actions

⚠️ CRITICAL BUG: Template actions are currently broken. Context variables are not passed to templates, causing all variable references to fail with UndefinedError. See Template Context section for details.

Template actions render Jinja2 templates with full workflow context, enabling dynamic file generation for configurations, documentation, and code files.

Configuration

[[actions]]
name = "action-name"
type = "template"
source_path = "template-file-or-directory"
destination_path = "output-location"

Fields

source_path (required)

Path to template file or directory. Can be:

  • Single .j2 template file
  • Directory containing .j2 files (recursively rendered)

Type: ResourceUrl (string path)

Conventions:

  • Template files should use .j2 extension
  • Directory paths should NOT include wildcards
  • Relative to working directory by default
  • Usually prefixed with workflow:/// for workflow resources

destination_path (required)

Output location for rendered templates.

Type: ResourceUrl (string path)

Behavior:

  • For single file: Output file path
  • For directory: Output directory (structure mirrored)
  • Parent directories created automatically
  • Existing files overwritten

Template Context

⚠️ KNOWN ISSUE: The current implementation does NOT pass context variables to templates. Template variables like {{ imbi_project.name }} will raise UndefinedError until this bug is fixed.

Expected variables (not currently available):

Variable Type Description
workflow Workflow Current workflow configuration
imbi_project ImbiProject Complete Imbi project data
github_repository GitHubRepository GitHub repo data (if applicable)
working_directory Path Workflow execution directory
starting_commit str Initial Git commit SHA

Technical Details:

The template action calls prompts.render(self.context, source_path) but does not pass **self.context.model_dump() as kwargs, unlike shell and claude actions which do pass context variables. This means all template examples below will fail in the current implementation.

Examples

Single File Template

Workflow config:

[[actions]]
name = "generate-readme"
type = "template"
source_path = "workflow:///templates/README.md.j2"
destination_path = "repository:///README.md"

Template (templates/README.md.j2):

# {{ imbi_project.name }}

{{ imbi_project.description }}

## Project Information

- **Type**: {{ imbi_project.project_type }}
- **Namespace**: {{ imbi_project.namespace }}
- **Imbi URL**: {{ imbi_project.imbi_url }}

{% if github_repository %}
## Repository

- **GitHub**: {{ github_repository.html_url }}
- **Default Branch**: {{ github_repository.default_branch }}
{% endif %}

## Installation

```bash
pip install {{ imbi_project.slug }}

Generated by {{ workflow.configuration.name }} on {{ now() }}

### Directory Template

**Workflow config:**
```toml
[[actions]]
name = "render-configs"
type = "template"
source_path = "workflow:///templates/configs"
destination_path = "repository:///config/"

Directory structure:

workflow/templates/configs/
├── app.yaml.j2
├── database.yaml.j2
└── logging.yaml.j2

Result:

repository/config/
├── app.yaml
├── database.yaml
└── logging.yaml

GitHub Actions Workflow Template

Workflow config:

[[actions]]
name = "generate-ci-workflow"
type = "template"
source_path = "workflow:///ci-template.yml.j2"
destination_path = "repository:///.github/workflows/ci.yml"

Template:

name: CI

on:
  push:
    branches: [ {{ github_repository.default_branch }} ]
  pull_request:
    branches: [ {{ github_repository.default_branch }} ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      {% if imbi_project.facts.get('Programming Language', '').startswith('Python') %}
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '{{ imbi_project.facts['Programming Language'].split()[-1] }}'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -e .[dev]

      - name: Run tests
        run: pytest tests/ -v
      {% elif imbi_project.facts.get('Programming Language') == 'JavaScript' %}
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test
      {% endif %}

Dockerfile Template

Workflow config:

[[actions]]
name = "generate-dockerfile"
type = "template"
source_path = "workflow:///Dockerfile.j2"
destination_path = "repository:///Dockerfile"

Template:

{% set python_version = imbi_project.facts.get('Programming Language', 'Python 3.12').split()[-1] %}
FROM python:{{ python_version }}-slim

LABEL maintainer="{{ imbi_project.namespace }}"
LABEL project="{{ imbi_project.slug }}"

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

{% if imbi_project.project_type == 'api' %}
CMD ["uvicorn", "{{ imbi_project.slug }}.main:app", "--host", "0.0.0.0", "--port", "8000"]
{% elif imbi_project.project_type == 'consumer' %}
CMD ["python", "-m", "{{ imbi_project.slug }}.consumer"]
{% else %}
CMD ["python", "-m", "{{ imbi_project.slug }}"]
{% endif %}

Jinja2 Template Features

Variables

{{ imbi_project.name }}
{{ imbi_project.slug }}
{{ workflow.configuration.name }}

Conditionals

{% if imbi_project.project_type == 'api' %}
  API-specific content
{% elif imbi_project.project_type == 'consumer' %}
  Consumer-specific content
{% else %}
  Default content
{% endif %}

Loops

{% for env in imbi_project.environments %}
- {{ env.name }}: {{ env.url }}
{% endfor %}

Filters

{{ imbi_project.name | upper }}
{{ imbi_project.slug | replace('-', '_') }}
{{ imbi_project.description | truncate(100) }}

Tests

{% if github_repository is defined %}
  Has GitHub repository
{% endif %}

{% if imbi_project.facts %}
  Has facts defined
{% endif %}

Comments

{# This is a comment - won't appear in output #}
{{ imbi_project.name }}  {# inline comment #}

Common Patterns

Configuration File Generation

[[actions]]
name = "generate-app-config"
type = "template"
source_path = "workflow:///app.yaml.j2"
destination_path = "repository:///config/app.yaml"

Template:

application:
  name: {{ imbi_project.slug }}
  type: {{ imbi_project.project_type }}

{% if imbi_project.environments %}
environments:
{% for env in imbi_project.environments %}
  {{ env.name }}:
    url: {{ env.url }}
    enabled: true
{% endfor %}
{% endif %}

database:
  host: ${DB_HOST}
  port: ${DB_PORT}
  name: {{ imbi_project.slug | replace('-', '_') }}

Multi-File Template Directory

[[actions]]
name = "generate-all-configs"
type = "template"
source_path = "workflow:///templates"
destination_path = "repository:///config/"

Template directory:

workflow/templates/
├── app.yaml.j2
├── database.yaml.j2
├── logging.yaml.j2
└── monitoring.yaml.j2

Documentation Generation

[[actions]]
name = "generate-docs"
type = "template"
source_path = "workflow:///docs"
destination_path = "repository:///docs/"

Template:

# {{ imbi_project.name }} Documentation

## Overview
{{ imbi_project.description }}

## Quick Start

### Installation
```bash
pip install {{ imbi_project.slug }}

Usage

from {{ imbi_project.slug | replace('-', '_') }} import main

main()

API Reference

{% if imbi_project.project_type == 'api' %} The API is available at: https://{{ imbi_project.slug }}.example.com/api {% endif %}

Contributing

Contributions welcome! See CONTRIBUTING.md in the repository for details.


Generated from template by {{ workflow.configuration.name }}

## Advanced Templates

### Accessing Nested Data

```jinja2
{# Access project facts #}
{% if 'Programming Language' in imbi_project.facts %}
Language: {{ imbi_project.facts['Programming Language'] }}
{% endif %}

{# Access GitHub repository details #}
{% if github_repository %}
Stars: {{ github_repository.stargazers_count }}
Forks: {{ github_repository.forks_count }}
{% endif %}

Template Inheritance

Base template (base.j2):

# {{ imbi_project.name }}

{% block content %}
Default content
{% endblock %}

---
Generated by {{ workflow.configuration.name }}

Child template (readme.j2):

{% extends "base.j2" %}

{% block content %}
## Description
{{ imbi_project.description }}

## Installation
pip install {{ imbi_project.slug }}
{% endblock %}

Macros

{% macro render_environment(env) %}
## {{ env.name | title }}
- URL: {{ env.url }}
- Active: {{ env.active | default(true) }}
{% endmacro %}

# Environments

{% for env in imbi_project.environments %}
{{ render_environment(env) }}
{% endfor %}

Custom Filters

{# Convert kebab-case to snake_case #}
{{ imbi_project.slug | replace('-', '_') }}

{# Convert to SCREAMING_SNAKE_CASE #}
{{ imbi_project.slug | replace('-', '_') | upper }}

{# Truncate long descriptions #}
{{ imbi_project.description | truncate(100, True, '...') }}

Path Resolution

Both source and destination support ResourceUrl schemes:

# Workflow templates to repository
[[actions]]
type = "template"
source_path = "workflow:///templates/"
destination_path = "repository:///config/"

# Specific file paths
[[actions]]
type = "template"
source_path = "workflow:///README.md.j2"
destination_path = "repository:///README.md"

# Extracted data with templates
[[actions]]
type = "template"
source_path = "workflow:///process-extracted.j2"
destination_path = "extracted:///processed/output.txt"

Error Handling

Template actions raise errors for:

  • Missing source: Source file/directory doesn't exist
  • Template syntax errors: Invalid Jinja2 syntax
  • Undefined variables: Referenced variables not in context
  • I/O errors: Permission denied, disk full, etc.

Handling Undefined Variables

Strict mode (default - raises error):

{{ undefined_variable }}  {# Raises error #}

Graceful fallback:

{{ undefined_variable | default('fallback value') }}
{{ undefined_variable | default('') }}

Check before use:

{% if undefined_variable is defined %}
  {{ undefined_variable }}
{% endif %}

Implementation Notes

  • Templates rendered using Jinja2 with StrictUndefined by default
  • BUG: Context variables NOT passed to templates (missing **context.model_dump() in render calls)
  • .j2 extension NOT automatically removed from output filenames
  • Directory rendering is recursive via source_path.rglob('*')
  • Existing files overwritten without warning
  • Parent directories created automatically with mkdir(parents=True, exist_ok=True)
  • File permissions NOT explicitly preserved (uses default umask)
  • Template context is immutable during rendering
  • All Jinja2 built-in filters and tests available (if context were passed)
  • Only extract_image_from_dockerfile custom function added to globals

Implementation Location: src/imbi_automations/actions/template.py:24-82

Bug Details:

# Current broken implementation (line 52, 73):
prompts.render(self.context, source_path)

# Should be (like shell.py:130 and claude.py:130):
prompts.render(self.context, source_path, **self.context.model_dump())

Best Practices

  1. Use .j2 extension: Makes templates easily identifiable
  2. Provide defaults: Use | default() for optional values
  3. Check existence: Test is defined before accessing optional data
  4. Document templates: Add comments explaining template logic
  5. Test templates: Verify output with sample data before production
  6. Version control: Keep templates in workflow resources
  7. Validate output: Use shell actions to validate generated configs