An AI-generated project summary should not become a client-ready report just because a template rendered. Missing fields, unexpected markup and unreviewed claims are different problems. Put a small, explicit boundary between generated text and the HTML your business delivers.

This example uses the open-source Jinja template engine to render two plain-text fields into an application-owned report. It rejects malformed input, raises on undefined template variables and escapes inserted HTML characters. It deliberately does not send the report or approve its claims.

The fictional business task

Fictional scenario: Orbit & Sons needs a draft booking-workflow review. The sample summary says to review the workflow before launch. These are authored fixtures, not a real customer, a model response or Jason Ead’s personal experience. Imagine an AI step produced the two fields; the code below tests the rendering boundary without calling an AI model.

Input case Observed result Next step
Orbit & Sons plus plain summary HTML draft with escaped ampersand Human reviews content
Missing summary ValueError Repair input
Summary contains a script tag Tag rendered as text Review suspicious content
Summary contains {{ 7 * 7 }} Expression stays literal Review content; no second render
Explanatory result map from the executed tests. Escaped text can still be inaccurate or inappropriate; successful rendering is not delivery approval.

Why Jinja fits this small job

Jinja separates a template from the values passed to it. Configure autoescaping explicitly instead of relying on a default. Here the template is a constant owned by the application, and input values appear only as HTML text content. Jinja API documentation.

The pinned 3.1.6 runtime implements StrictUndefined by raising for printing and other common operations on missing variables. That catches template wiring mistakes; it does not validate business facts. Our input gate catches missing fields earlier, and a separate test deliberately bypasses that gate to exercise StrictUndefined itself. Versioned runtime source.

The project is distributed under BSD three-clause terms; retain required notices when redistributing it. Its metadata declares MarkupSafe as a dependency. This is a focused fit assessment, not a security audit of the repository or all its dependencies. License; project metadata.

Build the renderer

Prerequisites: Python with Jinja2 3.1.6 installed in your chosen environment. This run used Windows, Python 3.14.3, Jinja2 3.1.6 and MarkupSafe 3.0.3. Existing installed packages were used; a clean package-install or cross-platform test is not claimed. Save this as report.py in a disposable project folder.

"""Render plain-text business fields through an application-owned template."""
from jinja2 import Environment, StrictUndefined

ENV = Environment(autoescape=True, undefined=StrictUndefined)
TEMPLATE = ENV.from_string('''<!doctype html>
<html lang="en"><meta charset="utf-8"><title>Review draft</title>
<h1>Review draft for {{ customer }}</h1>
<p>{{ summary }}</p>
<p>Human review required before delivery.</p></html>''')


def render_report(data):
    if type(data) is not dict or set(data) != {'customer', 'summary'}:
        raise ValueError('Exactly customer and summary are required')
    for key, limit in [('customer', 100), ('summary', 2000)]:
        value = data[key]
        # Exact str also excludes Markup and custom __html__ objects.
        if type(value) is not str or not value.strip() or len(value) > limit:
            raise ValueError(f'{key} must be nonblank plain text, max {limit} characters')
    return TEMPLATE.render(**data)


if __name__ == '__main__':
    print(render_report({'customer': 'Orbit & Sons',
                         'summary': 'Draft: review the booking workflow before launch.'}))

There are three boundaries here. First, exactly two named fields are allowed, so an unexpected approval flag cannot quietly affect the report. Second, values must be nonblank plain strings within the stated character limits. The exact-type check also excludes Markup objects and custom HTML-aware objects. Third, only the fixed application template is compiled; a value containing template syntax is not compiled again.

Never replace that constant with model-generated template source, add a safe-markup bypass or run the rendered output through Jinja a second time. This Environment is not a sandbox for executing untrusted templates. Autoescaping here covers inserted HTML text, not arbitrary JavaScript, CSS, URLs or attribute contexts. The report has none of those dynamic contexts.

Reproduce the behavior, including failures

Save the following as test_report.py beside the renderer:

import unittest
from jinja2 import UndefinedError
from markupsafe import Markup
from report import render_report, TEMPLATE


class ReportTests(unittest.TestCase):
    def setUp(self):
        self.data = {'customer': 'Orbit & Sons', 'summary': 'Review booking.'}

    def test_normal(self):
        result = render_report(self.data)
        self.assertIn('Orbit &amp; Sons', result)
        self.assertIn('Human review required before delivery.', result)

    def test_missing(self):
        del self.data['summary']
        with self.assertRaises(ValueError): render_report(self.data)

    def test_extra(self):
        self.data['approved'] = True
        with self.assertRaises(ValueError): render_report(self.data)

    def test_blank(self):
        self.data['summary'] = '  '
        with self.assertRaises(ValueError): render_report(self.data)

    def test_null(self):
        self.data['summary'] = None
        with self.assertRaises(ValueError): render_report(self.data)

    def test_oversize(self):
        self.data['summary'] = 'x' * 2001
        with self.assertRaises(ValueError): render_report(self.data)

    def test_markup_not_trusted(self):
        self.data['summary'] = Markup('<b>approved</b>')
        with self.assertRaises(ValueError): render_report(self.data)

    def test_html_is_text(self):
        self.data['summary'] = '<script>alert(1)</script>'
        result = render_report(self.data)
        self.assertNotIn('<script>', result)
        self.assertIn('&lt;script&gt;alert(1)&lt;/script&gt;', result)

    def test_template_expression_is_text(self):
        self.data['summary'] = '{{ 7 * 7 }}'
        self.assertIn('{{ 7 * 7 }}', render_report(self.data))

    def test_strict_undefined(self):
        with self.assertRaises(UndefinedError):
            TEMPLATE.render(customer='Orbit')

    def test_input_unchanged_and_repeatable(self):
        original = self.data.copy()
        self.assertEqual(render_report(self.data), render_report(self.data))
        self.assertEqual(self.data, original)


if __name__ == '__main__':
    unittest.main()

Run the tests, then print the sample HTML:

python -X utf8 -m unittest -v test_report.py
python -X utf8 report.py

All eleven tests passed in the recorded environment. They cover ordinary output, missing/extra fields, blank/null/oversized values, refusal to trust a Markup object, escaped script text, literal template syntax, StrictUndefined behavior and repeatable rendering without changing the input dictionary. The sample program printed:

<!doctype html>
<html lang="en"><meta charset="utf-8"><title>Review draft</title>
<h1>Review draft for Orbit &amp; Sons</h1>
<p>Draft: review the booking workflow before launch.</p>
<p>Human review required before delivery.</p></html>

The final article’s renderer and test blocks were extracted into a disposable directory and executed again. That is a runtime reproduction of this example, not a claim about your email platform or customer application.

Connect it to an AI workflow without making rendering approval

Have the upstream step return structured customer and summary values. Parse the payload, validate its business meaning, then call the renderer. Treat ValueError or UndefinedError as a blocked draft needing attention; do not catch the error and send an empty report. For a fuller structured-data boundary, see validating AI output before a CRM handoff.

After rendering, a reviewer still checks the named customer, source evidence, scope and promises. The “Human review required” footer is only a label. It does not enforce approval. A production system needs an actual approved state tied to the exact report, appropriate reviewer access and a separate authorized delivery step.

The example’s script-looking text is an adversarial fixture; ordinary text can be harmful or false too. Escaping markup does not remove prompt injection, verify facts, redact private information or make a report suitable for a customer. Keep source data and review evidence available to the person making that decision.

Limits that matter before production

The program returns an HTML string and prints a sample. It does not write files, send email, call models, connect a CRM or deploy an app. Character bounds apply after a Python object already exists, so bound request size before parsing in a real service. No browser compatibility suite, PDF engine, email-client rendering, authenticated approval, load test or repository-wide security review was performed. The cover is generated conceptual artwork; the interior table describes local test outcomes.

Jason Ead publishes AlgoThoughts AI News and connects practical engineering with My Biz Heroes: websites, automation, workflows, AI tools and training. Explore Jason’s work. The business payoff here is a report pipeline that makes missing data visible before a reviewer approves delivery.