Chapter 7: Rendering with Templates

Using Jinja2 for Modular and Dynamic ChatML Message Generation

Abstract

This chapter introduces Jinja2 templating as a foundational technique for generating modular and dynamic ChatML messages.

It demonstrates how templates can encode context-aware roles, structured message composition, and parameterized logic — enabling the same ChatML schema to adapt fluidly across tasks and agents.

Through practical examples from the Project Support Bot, you’ll learn to integrate Jinja2 into your ChatML pipeline to produce clear, maintainable, and reproducible conversation logic.

Keywords

ChatML, LLMs, Prompt Engineering, LangChain, LlamaIndex

7: Rendering with Templates

7.1 Introduction: Why Templating Matters

The ChatML format gives us structure and hierarchy, but real-world systems demand flexibility.

As the Project Support Bot grows, it must generate hundreds of messages dynamically:

  • System prompts that vary by project type
  • User queries with variable placeholders
  • Assistant reasoning templates for reports and summaries
  • Tool calls with injected parameters (like sprint names, user IDs, or deadlines)

Writing each message manually becomes error-prone and inconsistent.

The solution is templating — a declarative way to express message blueprints that can be dynamically rendered at runtime.

Jinja2, a Python-based templating engine, provides the perfect foundation for this task.


7.2 The Role of Jinja2 in ChatML

Jinja2 brings two essential capabilities to ChatML generation:

  1. Parameterization – allowing placeholders ({ variable }) within messages for dynamic data injection.
  2. Control Structures – supporting conditional logic ({% if %}, {% for %}) to adapt the structure of messages at render time.

This aligns perfectly with ChatML’s philosophy of structured reproducibility with flexible semantics.

Example

<|im_start|>user
Please summarize the project "{{ project_name }}" for sprint {{ sprint_number }}.
<|im_end|>

Rendered dynamically:

<|im_start|>user
Please summarize the project "Apollo" for sprint 7.
<|im_end|>

7.3 Jinja2 Setup and Configuration

Installation

pip install Jinja2

Basic Usage

from jinja2 import Template

template = Template("Hello {{ name }}, welcome to the project support bot.")
output = template.render(name="Ranjan")
print(output)

Output:

Hello Ranjan, welcome to the project support bot.

This same logic extends to generating full ChatML message blocks.


7.4 Building ChatML Templates

A System Message Template

<|im_start|>system
You are a project support assistant helping Agile teams.
Current date: {{ current_date }}
Project: {{ project_name }}
<|im_end|>

Rendered in Python:

from jinja2 import Environment, FileSystemLoader
from datetime import date

env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("system_message.jinja2")

message = template.render(current_date=date.today(), project_name="NeelGarh AI Tracker")
print(message)

Output:

<|im_start|>system
You are a project support assistant helping Agile teams.
Current date: 2025-11-11
Project: NeelGarh AI Tracker
<|im_end|>

This makes system initialization contextual yet consistent.


7.5 Using Templating for Role Logic

Templating also allows role-specific logic to be dynamically configured.

Multi-Role Template Example

<|im_start|>user
{{ user_query }}
<|im_end|>

<|im_start|>assistant
{% if context %}
Based on the previous sprint report: {{ context }}
{% endif %}
Processing your request now...
<|im_end|>

<|im_start|>tool
{{ tool_call }}
<|im_end|>

Rendered dynamically:

<|im_start|>user
Generate sprint summary for Sprint 8.
<|im_end|>

<|im_start|>assistant
Based on the previous sprint report: Velocity increased by 12%.
Processing your request now...
<|im_end|>

<|im_start|>tool
fetch_sprint_data(sprint="Sprint 8")
<|im_end|>

This maintains ChatML’s structural clarity while letting logic adapt dynamically.


7.6 Dynamic Context Injection

The Jinja2 environment can inject runtime metadata into messages — project name, user role, timestamps, or tool responses.

context = {
    "user_query": "Summarize sprint backlog.",
    "context": "5 open tickets from last sprint.",
    "tool_call": "get_open_tickets(project='Apollo')"
}

template = env.get_template("conversation_flow.jinja2")
rendered = template.render(**context)

This allows ChatML pipelines to remain data-driven while preserving role boundaries.


7.7 Template Composition and Includes

For large-scale systems, it’s wise to break templates into modular components.

Jinja2 supports reusable includes and extends directives.

Example: Splitting Roles into Separate Templates

base_message.jinja2

<|im_start|>{{ role }}
{{ content }}
<|im_end|>

assistant_response.jinja2

{% extends "base_message.jinja2" %}
{% block content %}
Hello {{ user_name }}, I’ve processed your sprint data.
Velocity: {{ velocity }} points.
{% endblock %}

Render call:

output = env.get_template("assistant_response.jinja2").render(user_name="Ranjan", velocity=42)

Result:

<|im_start|>assistant
Hello Ranjan, I’ve processed your sprint data.
Velocity: 42 points.
<|im_end|>

This modularization ensures reusability and maintainability across prompts.


7.8 Advanced Logic and Filters

Jinja2 supports filters, loops, and conditionals — making it perfect for generating structured multi-message outputs.

{% for sprint in sprints %}
<|im_start|>assistant
Sprint {{ sprint.number }} summary:
Velocity: {{ sprint.velocity }} points
Issues Closed: {{ sprint.closed }}
<|im_end|>
{% endfor %}

Render call:

data = {"sprints": [{"number": 1, "velocity": 38, "closed": 12}, {"number": 2, "velocity": 42, "closed": 15}]}
template = env.get_template("sprint_summary.jinja2")
print(template.render(**data))

This produces dynamic, multi-block ChatML messages with programmatic iteration — ideal for dashboards and report generation.


7.9 Integrating Jinja2 into the ChatML Pipeline

Once templating is established, it integrates naturally with the pipeline architecture from Chapter 6.

Integration Steps

  1. Prepare Context – Gather user inputs, history, and environment metadata.
  2. Select Template – Choose an appropriate Jinja2 file for the role or task.
  3. Render ChatML – Use template.render() to generate the message text.
  4. Pass to Model – Send the rendered message to the LLM or orchestration agent.
  5. Capture Output – Decode responses as ChatML messages for persistence.

Example Integration

def build_chatml_message(role, template_name, context):
    env = Environment(loader=FileSystemLoader("templates"))
    template = env.get_template(template_name)
    return {"role": role, "content": template.render(**context)}

Used in sequence:

messages = [
    build_chatml_message("system", "system_message.jinja2", {"project_name": "Apollo"}),
    build_chatml_message("user", "user_request.jinja2", {"task": "Generate sprint summary"}),
]

The output remains completely ChatML-compliant while being fully automated.


7.10 Error Handling and Validation

Dynamic templating introduces risks: missing variables, incorrect syntax, or mismatched roles.

The pipeline should validate and sanitize templates before execution.

Example:

try:
    rendered = template.render(**context)
except Exception as e:
    log_error(f"Template rendering failed: {e}")

Validation can also enforce that every rendered template produces balanced ChatML markers (<|im_start|>, <|im_end|>), ensuring structural integrity.


7.11 Rendering Strategy for Project Support Bot

In the Project Support Bot, templates can be categorized as:

Template Type Purpose Example File
System Templates Define assistant identity and rules system_init.jinja2
User Templates Capture user queries and variations user_query.jinja2
Assistant Templates Generate structured reasoning assistant_reasoning.jinja2
Tool Templates Create reproducible tool calls tool_action.jinja2
Report Templates Format summaries and outputs sprint_report.jinja2

This modular structure keeps communication logic organized across the entire lifecycle.


7.12 Debugging and Visualization

To debug Jinja2 rendering, the pipeline can log:

  • Template name used
  • Variables injected
  • Final rendered output

Visualization in FigJam or documentation tools can illustrate template dependencies:

System → User → Assistant → Tool → Assistant → User

Each role maps to a Jinja2 template, ensuring maintainability at scale.


7.13 Engineering for Scalability and Trust

Templating brings maintainability and governance to ChatML-based systems.

Design Value Achieved Through
Structure Templates enforcing ChatML schema
Flexibility Variables and conditional logic
Reproducibility Deterministic rendering across environments
Transparency Clear separation of message roles
Maintainability Reusable and versioned template library

7.14 Summary

Aspect Description Implementation
Templating Engine Renders dynamic ChatML content Jinja2
Template Types System, User, Assistant, Tool, Report .jinja2 files
Context Handling Inject variables and runtime data template.render(**context)
Integration Plug into ChatML pipeline Message builder pattern
Governance Versioned templates Template registry and logging

7.15 Closing Thoughts

Where Chapter 6 established the ChatML pipeline as a structured conversation framework, Chapter 7 introduces templating as its creative and dynamic extension.

By integrating Jinja2, ChatML evolves from static message construction to adaptive prompt generation — enabling assistants that speak naturally yet operate reproducibly.

For the Project Support Bot, this means:

  • Each project gets contextualized system prompts.
  • Reports and summaries are formatted automatically.
  • Every ChatML message remains verifiable and auditable.