Skip to main content
I built Luria so a single set of prompts could carry me through every evaluation I run — pediatric or adult, one session or three. Jinja2 templating is how I make that possible. Instead of editing patient names and chief complaints into a prompt every time, you write each prompt in agents/prompts/PROMPTS.md once with {{variable}} placeholders. At runtime, you fill them in from a patient context dictionary. This guide walks through how the templating system works, which variables are standardized across all prompts, and how to wire the rendered prompts into your agent pipeline.
Templating only changes how prompts are assembled. The clinical instructions — tone, methodology, output rules — stay hardcoded in PROMPTS.md. You are parameterizing the patient-specific content, not the clinical reasoning.

When to use templates

Use a {{variable}} when the value changes between patients, domains, or visits:
Use a template forExample
Patient-specific fields{{patient_first_name}}, {{patient_age}}, {{chief_complaint}}
Domain-specific fields{{domain_name}}, {{domain_scores}}
Optional sections{% if has_parent_rater %}...{% endif %}
Variable-length lists{% for rec in recommendations %}...{% endfor %}
Hardcode anything that should stay identical across every evaluation:
Hardcode insteadExample
Clinical instructions”Write in third-person past tense”
Methodology”Use a Chain of Density approach”
Output format rules”Return clean markdown only”
Fixed domain definitionsThe canonical list of cognitive domains

Three pieces of syntax

You only need three Jinja2 constructs to template every prompt in Luria.

1. Variable substitution

Patient {{patient_first_name}}, age {{patient_age}}, presented with {{chief_complaint}}.
Rendered with {"patient_first_name": "Sarah", "patient_age": 14, "chief_complaint": "difficulty concentrating"}, this becomes:
Patient Sarah, age 14, presented with difficulty concentrating.

2. Conditional blocks

Use {% if %} / {% else %} / {% endif %} for content that only applies to some patients:
{% if has_parent_rater %}
Parent and patient ratings aligned on {{domain_name}} strengths.
{% else %}
Patient self-report on {{domain_name}} unavailable; clinician observation only.
{% endif %}

3. Loops

Use {% for %} / {% endfor %} to render a list whose length you do not know in advance — recommendations, history items, validity concerns:
### School / Academic

{% for rec in academic_recommendations %}
- {{rec}}
{% endfor %}

Standard variables

These variables are loaded from config.patient.yml and available to every prompt. Use them consistently so a single context dictionary works across the whole pipeline.
VariableTypeExample
patient_first_namestring"Sarah"
patient_last_namestring"Chen"
patient_full_namestring"Sarah Chen"
patient_ageinteger14
patient_dobstring"2010-05-15"
patient_pronounsstring"she/her"
he_shestring"she"
his_herstring"her"
evaluation_datestring"May 15, 2026"
referral_sourcestring"PCP Dr. Smith"
chief_complaintstring"difficulty concentrating"
clinician_namestring"Joey Trampush, Ph.D."
clinic_namestring"Brainworkup Neuropsychology, LLC"

Domain-specific variables

These apply only to domain-level prompts such as DOMAIN_INTERPRETATION and SIRF_SYNTHESIS. Pass them per call, not from the global patient config.
VariableTypeExample
domain_namestring"Memory"
domain_typestring"neurocog" or "neurobehav"
domain_scoresdict{"test1": 65, "test2": 62}
has_parent_raterbooleantrue
has_teacher_raterbooleantrue
validity_concernslist["low effort", "response bias"]
Use snake_case for every variable name. Jinja2 substitution is case-sensitive — {{patient_first_name}} and {{patientFirstName}} are not the same variable.

Templated example: NSE_COD_SUMMARY

This is what a real prompt section in PROMPTS.md looks like after templating. Note how the clinical role and methodology remain hardcoded while patient identifiers and history fields use placeholders.
## NSE_COD_SUMMARY

**Role:** Summarize NSE transcript using Chain of Density (CoD) approach.

**Worker:** `Qwen3.6-35B-A3B-oQ4`

**Input:** `{{transcript_file}}`

**Output:** `data/intake/nse_summary_redacted.md` (post-Presidio redaction)

You are an experienced, board-certified clinical neuropsychologist with expertise
in psychodiagnostic assessment and neurobehavioral examination.

```markdown
# NEUROBEHAVIORAL STATUS EXAM SUMMARY

## Identifying Information and Reason for Referral

**Patient:** {{patient_full_name}}
**Age:** {{patient_age}} years
**Date of Evaluation:** {{evaluation_date}}
**Referral Source:** {{referral_source}}
**Reason for Evaluation:** {{chief_complaint}}

## Background and History

### Medical History
{{medical_history}}

### Psychiatric History
{{psychiatric_history}}
```

Rendering a prompt from Python

Install jinja2 in your Luria environment (it is already a dependency of the agent pipeline). Then load PROMPTS.md as a template and render it against a context dictionary.
from jinja2 import Environment, FileSystemLoader

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

template = env.get_template("PROMPTS.md")

context = {
    "patient_first_name": "Sarah",
    "patient_full_name": "Sarah Chen",
    "patient_age": 14,
    "chief_complaint": "difficulty concentrating",
    "referral_source": "PCP Dr. Smith",
    "evaluation_date": "May 15, 2026",
    "he_she": "she",
    "his_her": "her",
}

filled_prompt = template.render(context)

Rendering a single prompt section

If you only need one section — say, NSE_COD_SUMMARY — extract it first with a regex, then render the snippet directly with jinja2.Template:
import re
from jinja2 import Template

with open("agents/prompts/PROMPTS.md") as f:
    full_file = f.read()

match = re.search(
    r"## NSE_COD_SUMMARY\n(.*?)(?=\n## |\Z)",
    full_file,
    re.DOTALL,
)

if match:
    section = match.group(1)
    filled = Template(section).render(context)

A reusable helper for the agent pipeline

In practice, every agent in the pipeline ends up calling the same render step. Wrap it in a helper so each agent only needs to pass its context:
# agents/pipeline/phase_a_nse/nse_cod_summary.py

from jinja2 import Template

NSE_COD_SUMMARY_TEMPLATE = """\
You are an experienced clinical neuropsychologist...

**Patient:** {{patient_full_name}}
**Age:** {{patient_age}} years
**Date of Evaluation:** {{evaluation_date}}
**Referral Source:** {{referral_source}}
**Reason for Evaluation:** {{chief_complaint}}

### Medical History
{{medical_history}}

### Psychiatric History
{{psychiatric_history}}
"""


def fill_nse_cod_prompt(patient_context: dict) -> str:
    """Render NSE_COD_SUMMARY with patient-specific data."""
    return Template(NSE_COD_SUMMARY_TEMPLATE).render(patient_context)
Then in the agent:
context = {
    "patient_full_name": "Sarah Chen",
    "patient_age": 14,
    "evaluation_date": "May 15, 2026",
    "referral_source": "PCP Dr. Smith",
    "chief_complaint": "difficulty concentrating and organizing",
    "medical_history": "No significant medical history.",
    "psychiatric_history": "No prior psychiatric diagnoses.",
}

prompt = fill_nse_cod_prompt(context)
response = model.generate(prompt)

Common mistakes

Printing a string that contains {{patient_name}} does not substitute anything. You must construct a Template (or load it through an Environment) and call .render(context) for substitution to happen.
Jinja2 silently renders missing variables as an empty string. The prompt still runs, but you get sentences like "Patient Sarah is years old". Validate your context dictionary against the variables used in each prompt before sending it to a model.
A template that reads {{patientName}} will not be filled by a context with key patient_name. Pick snake_case and stick with it everywhere — in config.patient.yml, in PROMPTS.md, and in your Python code.

Workflow recommendation

  1. Add {{variable}} placeholders to PROMPTS.md in agents/prompts/, starting with patient identifiers and chief complaint.
  2. Build a context dictionary from config.patient.yml once per evaluation.
  3. For each agent in the pipeline, pass that context (plus any domain-specific keys) into the render helper.
  4. Send the rendered prompt to the model. Patient data never lives in PROMPTS.md itself, which keeps the prompt library reusable and PHI-free.
For deeper integration patterns and per-prompt variable maps, see agents/prompts/PROMPTS_JINJA_INTEGRATION.md in the Luria source tree.
Last modified on June 3, 2026