ADR-0032: Validate Jinja templates server-side by parsing the AST
ACCEPTED
Context
The Email and Template pipeline nodes accept Jinja templates authored by users. Syntax errors were only discovered at run time, deep inside a trace, with no edit-time feedback. We render with SandboxedEnvironment, so any validation must use the same sandbox to give faithful results — undefined-variable errors are only knowable at run time and cannot be flagged early, but pure syntax errors can.
A second-class authoring path also exists: pipelines can be updated through the API without the editor, so editor-only validation would leave a gap.
Decision
We will validate Jinja template syntax server-side by parsing the AST (never rendering), at two layers:
- Edit-time — a team-scoped
POST validate-jinja/endpoint (gated bylogin_and_team_required+pipelines.view_pipeline) parses the template viaSandboxedEnvironment.parse()and returns structured{line, column, message, severity}errors. A CodeMirrorlinter()extension in thejinja_templatewidget calls it on edit and renders inline diagnostics. The endpoint additionally runs djlint HTML linting (severitywarning) behind a curated rule allowlist (H020, H021, H025, T027, T034); all other djlint rules are dropped as noise for template fragments. - Save-time — a shared Pydantic
field_validatoron the template fields of both nodes calls the same parse helper, raisinginvalid_jinja_syntax. This is a backstop that catches syntax errors regardless of authoring path.
All parsing happens in Python; the client never parses Jinja, so editor feedback matches runtime behaviour exactly.
Consequences
- Syntax errors surface inline while typing and again on save, instead of only at run time.
- The endpoint and the save-time validator share one parse helper, so the two layers cannot diverge in what they accept.
- Undefined-variable errors are still invisible until run time — parsing cannot see them (handled at run time, see ADR-0033).
- djlint operates on files, so each HTML-lint request writes the template to a temp file (RAM-backed where available) and cleans it up — a small per-request cost accepted for the warnings it surfaces.
- The endpoint carries its own input guardrails (50,000-char cap, a
checksselector forjinja/html) since it takes raw request bodies. - Adds
@codemirror/lintas a frontend dependency.
Alternatives considered
- Client-side Jinja parsing — rejected; a JS parser would drift from the Python
SandboxedEnvironment, breaking parity between editor feedback and runtime. - Validate on
render()instead ofparse()— rejected; template variables are only available at run time, so rendering at edit/save time would raise spurious undefined-variable errors. - Editor-only validation — rejected; API-driven pipeline updates bypass the editor, so the Pydantic backstop is needed to close the gap.
- Full djlint rule set — rejected; most rules (DOCTYPE,
<title>, lang attributes,url_for) assume a complete HTML document and only produce noise on template fragments.