Chapter 7: Rendering with Templates
Using Jinja2 for Modular and Dynamic ChatML Message Generation
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.
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:
- Parameterization – allowing placeholders (
{ variable }) within messages for dynamic data injection.
- 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 Jinja2Basic 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
- Prepare Context – Gather user inputs, history, and environment metadata.
- Select Template – Choose an appropriate Jinja2 file for the role or task.
- Render ChatML – Use
template.render()to generate the message text.
- Pass to Model – Send the rendered message to the LLM or orchestration agent.
- 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.