Sociologix
← Latest in AI

Developer tools · · 5 min read

Jinja: catch missing fields in an AI brief draft

Render a trusted HTML template with explicit escaping and missing-field errors, then test where those checks end before adding an AI writer or approval flow.

By Sociologix Editorial

Official Jinja logo: a white torii gate on a pink and burgundy square beside black Jinja lettering.
Official Pallets/Jinja logo, retrieved November 9, 2026. Original mark colors and proportions preserved with white presentation padding.Image source ↗

Stop a missing owner from disappearing into a draft

An AI-generated project brief can look complete while omitting the person responsible for a task. Rendering that missing value as blank makes the omission harder to notice. A fixed template can instead fail when it tries to use the absent field.

This AI-assisted editorial guide tests that behavior with Jinja and a fictional local brief. It produces HTML marked as a draft for review or a needs_review result with no HTML. The exercise does not call a model, create a quote, approve a project, send email or connect to the Sociologix workspace.

1. Create a small, isolated template test

We verified Jinja2 3.1.6 on November 9, 2026. The package declares Python 3.7 or later; our local test used Python 3.13.15 on Windows, with MarkupSafe 3.0.4 installed as a dependency. [1] Jinja is distributed under the BSD-3-Clause license. [5]

Run these PowerShell commands in a new folder. Save the next block as brief_preview.py before the final command. Installation needs internet access; the example then runs with only its local fixture.

mkdir jinja-brief-demo
cd jinja-brief-demo
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install "Jinja2==3.1.6"
# Save brief_preview.py below first.
.\.venv\Scripts\python.exe brief_preview.py

2. Keep the template under application control

DictLoader holds one fixed template named brief.html. StrictUndefined raises when the template prints or iterates a missing value, while select_autoescape(["html"]) enables escaping for that template name. Configure the environment before loading the template. [2]

Pass the brief as data to render. Do not replace SOURCE with model-generated text or compile a customer’s text as a template. Our changing values appear only in HTML text positions; the template has no dynamic URLs, JavaScript or CSS.

preview_brief catches UndefinedError and returns a missing_template_field reason. It returns draft_only when rendering succeeds. Those are demonstration states defined by our adapter, and neither records approval. Both task-owner labels in the fixture are fictional role labels.

import copy
import json
import platform
from importlib.metadata import version

from jinja2 import (
    DictLoader, Environment, StrictUndefined, UndefinedError, select_autoescape,
)

# Trusted application template. Model output is passed only as data.
SOURCE = """<!doctype html>
<html lang="en"><head><meta charset="utf-8">
<title>Project brief draft</title>
</head><body><main>
<p>DRAFT — FOR REVIEW</p>
<h1>{{ brief["company"] }}</h1>
<p>{{ brief["goal"] }}</p>
<ul>
{% for task in brief["tasks"] %}
<li>{{ task["title"] }} — {{ task["owner"] }}</li>
{% endfor %}
</ul>
<p>No project approval recorded.</p>
</main></body></html>"""

ENV = Environment(
    loader=DictLoader({"brief.html": SOURCE}),
    undefined=StrictUndefined,
    autoescape=select_autoescape(["html"]),
)


def render_brief(brief):
    return ENV.get_template("brief.html").render(brief=brief)


def preview_brief(brief):
    try:
        html = render_brief(brief)
    except UndefinedError:
        return {"status": "needs_review", "reason": "missing_template_field"}
    return {"status": "draft_only", "html": html}


# Fictional local fixture, not a customer project or an approved proposal.
BRIEF = {
    "company": "Demo Harbor & Co.",
    "goal": "Route inbound questions for review",
    "tasks": [
        {"title": "Map the intake workflow", "owner": "Project lead"},
        {"title": "Check the handoff rules", "owner": "Operations reviewer"},
    ],
}

if __name__ == "__main__":
    missing_owner = copy.deepcopy(BRIEF)
    del missing_owner["tasks"][1]["owner"]
    print(json.dumps({
        "python": platform.python_version(), "jinja2": version("Jinja2"),
        "complete": preview_brief(BRIEF),
        "missing_owner": preview_brief(missing_owner),
    }, indent=2))

3. Read the observed output

The complete fixture returned draft_only with two list items. Demo Harbor & Co. appeared in the HTML source as Demo Harbor &amp; Co. The document contained DRAFT — FOR REVIEW and No project approval recorded.

Deleting owner from the second task returned exactly status needs_review and reason missing_template_field. There was no html key. The first task was valid, but that did not cause our adapter to return a partial document when the second task failed.

Our markup fixture placed a script-tag string in goal. The output contained escaped angle brackets and quotes rather than a script element. A separate goal containing {{ 7 * 7 }} remained that literal text instead of being evaluated as a second template.

Eleven local tests passed: complete draft structure, ampersand escaping, missing company, missing goal, missing task list, missing nested owner, plain markup escaping, literal template-looking data, an empty company, a None goal and an empty task list. These are controlled rendering checks, not a browser security audit or a test of an AI model.

4. Test the values that are present but unusable

StrictUndefined catches missing values that the executed template actually uses. It does not enforce a project-brief schema. In our tests, an empty company produced an empty heading, a None goal rendered as the word None, and an empty task list rendered with no list items.

A loop with no entries also never reaches its task-owner expression. Required fields cannot be inferred from checks inside a loop that does not run. Jinja’s template language supports conditions and defaults that can similarly change which values are accessed. [3]

Before a real integration, add separate input validation for nonempty company and goal strings, a bounded nonempty task list, and the required title and owner on each task. Check that the proposed tasks match the original request. The demonstration intentionally leaves those policy checks out so its limitations are visible.

5. Treat the output as a reviewed handoff

HTML escaping protects the text placements used here; it is not a general policy for every output context. Keep ordinary JSON-compatible values as data, and avoid bypassing escaping with safe-marking features. If a system accepts user-authored templates, it needs a separate sandbox and resource-control design. Jinja’s sandbox documentation describes those concerns and its limits. [4]

For a proposed AI workflow, preserve the original requirement and the validated structured brief alongside the template version. Render into a draft artifact, inspect it, and record approval through a separate application action. On a missing-field error, keep the review item open instead of silently substituting a placeholder owner.

Test that storage, previews and any eventual delivery step all retain the draft status until the intended approval occurs. This article tests the local renderer only; it does not implement persistence, access control, customer delivery or an approval service.

Sources & further reading

  1. Jinja2 on PyPI — package version and Python requirement
  2. Jinja API — Environment, loaders, StrictUndefined and autoescaping
  3. Jinja template documentation — variables, loops and undefined values
  4. Jinja sandbox documentation — untrusted templates and output limits
  5. Jinja license — BSD-3-Clause

Turn AI briefs into reviewable drafts

Bring Sociologix one intake brief and the fields your team needs before work can start. We can scope structured validation, a controlled document template and a review handoff around your existing process.

Talk to Sociologix