What Are Prompt Templates?

A prompt template is a string with placeholders that get filled in at runtime. Templates promote reuse, make prompts version-controllable, and separate prompt logic from application code.

Jinja2-Based Template Syntax

OpenClaw uses Jinja2 for prompt templating. Install if needed: pip install jinja2

from jinja2 import Template

tpl = Template("""
You are a {{ role }}.
Task: {{ task }}
Constraints: return only JSON.
""")
prompt = tpl.render(role="data analyst", task="summarise the CSV")

Variable Injection

# Template file: prompts/summarise.j2
"""
Summarise the following {{ document_type }} in {{ max_words }} words or fewer.
Focus on: {{ focus_topics | join(", ") }}.

Document:
{{ document }}
"""

# Python usage
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader("prompts/"))
tpl = env.get_template("summarise.j2")
prompt = tpl.render(
    document_type="research paper",
    max_words=150,
    focus_topics=["methodology", "results"],
    document=paper_text
)

Conditional Blocks

tpl = Template("""
Analyse the following code.
{% if include_security %}
Pay special attention to security vulnerabilities.
{% endif %}
{% if language %}
The code is written in {{ language }}.
{% endif %}
Code: {{ code }}
""")

Loops Over Lists

tpl = Template("""
Review the following items and provide feedback on each:
{% for item in items %}
{{ loop.index }}. {{ item }}
{% endfor %}
For each item, rate: quality (1-5) and suggest improvements.
""")

Nested Templates and Includes

# prompts/base.j2
{% include "prompts/role_section.j2" %}
{% include "prompts/task_section.j2" %}
{% block constraints %}
Return valid JSON only.
{% endblock %}

# prompts/analyst.j2
{% extends "prompts/base.j2" %}
{% block constraints %}
Return JSON: {"insights": list, "anomalies": list}
{% endblock %}

Managing a Prompt Library (YAML Promptbook)

# prompts/library.yaml
prompts:
  summarise:
    version: "2.1"
    template: |
      Summarise: {{ text }} in {{ words }} words.
  classify:
    version: "1.0"
    template: |
      Classify as positive/negative/neutral: {{ text }}
import yaml
from jinja2 import Template

with open("prompts/library.yaml") as f:
    library = yaml.safe_load(f)

def get_prompt(name: str, **kwargs) -> str:
    tpl = Template(library["prompts"][name]["template"])
    return tpl.render(**kwargs)

Error Handling in Templates

Jinja2 rendering fails silently by default when a variable is missing — it renders as an empty string, which produces invalid prompts. Enable StrictUndefined in production to surface missing variables immediately.

from jinja2 import Environment, StrictUndefined, FileSystemLoader

env = Environment(
    loader=FileSystemLoader("prompts/"),
    undefined=StrictUndefined,   # raise instead of silently inserting ''
    autoescape=False,            # prompts are not HTML
)

def render_prompt(template_name: str, **kwargs) -> str:
    try:
        return env.get_template(template_name).render(**kwargs)
    except Exception as exc:
        # Log the missing variable name, fall back to safe minimal prompt
        logger.error("Template render error in %s: %s", template_name, exc)
        raise PromptBuildError(f"Failed to render {template_name}") from exc

Template Security

Prompt templates can become injection vectors when user-supplied values are inserted verbatim. Always sanitize dynamic values before rendering:

  • Strip control characters. Remove null bytes and control characters from all user inputs: text.translate({0:None, **{c:None for c in range(1,32)}}).
  • Limit length. Enforce a maximum length on every user-supplied variable before inserting it into a template. A 10,000-token user input can hijack a carefully crafted system prompt.
  • Never use eval in templates. Jinja2's {% raw %}{% set %}{% endraw %} and filters are safe; Python-eval-based template engines are not. Do not pass user data to eval() or exec() inside templates.
  • Separate trust levels. Render system-prompt templates and user-content inserts with different variable namespaces so user data cannot overwrite a system variable.

Testing Templates

Templates should be tested without making real LLM calls — verify rendering correctness and variable injection independently:

import pytest
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader("prompts/"))

def render(name, **kw):
    return env.get_template(name).render(**kw)

def test_coding_template_includes_task():
    result = render("coding-agent.j2", task="write tests", language="Python")
    assert "write tests" in result
    assert "Python" in result

def test_coding_template_no_empty_placeholders():
    result = render("coding-agent.j2", task="x", language="Go")
    assert "{{" not in result  # no un-rendered Jinja variables

def test_report_template_formats_date():
    result = render("report.j2", report_date="2026-03-17", period="Q1")
    assert "2026-03-17" in result
    assert "Q1" in result