# How Jinja Templates Power Prompt Engineering for Resume Parsing in the Hiring-Agent Repository

> Discover how Jinja templates streamline prompt engineering for resume parsing in the hiring-agent repository. Generate dynamic LLM prompts and achieve deterministic JSON output.

- Repository: [HackerRank/hiring-agent](https://github.com/interviewstreet/hiring-agent)
- Tags: internals
- Published: 2026-07-19

---

**The Hiring-Agent repository utilizes Jinja templates to dynamically generate LLM prompts for resume section extraction, separating prompt logic from Python code and enabling deterministic JSON output through template variable injection.**

The interviewstreet/hiring-agent open-source project demonstrates a production-grade approach to resume parsing by leveraging **Jinja templates for prompt engineering**. Rather than hard-coding instruction strings inside Python classes, the repository stores modular prompt templates in a dedicated directory, allowing the system to inject raw resume markdown into pre-defined schemas. This architecture treats prompts as version-controlled assets, enabling rapid iteration on extraction logic without modifying core application code in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) or [`template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/template_manager.py).

## TemplateManager: The Rendering Engine

At the heart of the system lies the **TemplateManager** class defined in [`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py). During initialization (approximately lines 21-55), it creates a Jinja `Environment` object pointing to the `prompts/templates` folder and discovers all files ending in `.jinja`. This pre-loading step caches templates for sections including `basics.jinja`, `work.jinja`, `education.jinja`, `skills.jinja`, `projects.jinja`, `awards.jinja`, and the reusable `system_message.jinja`, ensuring low-latency rendering during PDF processing.

When a specific resume section requires extraction, handlers invoke `TemplateManager.render_template(section, **kwargs)`. This method injects variables—primarily `text_content` containing the raw resume markdown—into the template, returning a fully-formed prompt string ready for API transmission.

## From Disk Template to Structured Data: The Execution Flow

### Step 1: Loading Section-Specific Prompts

Each resume section maintains its own Jinja template to handle distinct data structures. For example, `basics.jinja` focuses on contact information and profiles, while `work.jinja` targets employment history. The `TemplateManager` stores these in memory after scanning the `prompts/templates` directory at startup.

### Step 2: Dynamic Rendering in PDF Handlers

Within [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py), methods matching the pattern `extract_*_section` (lines 36-65) orchestrate the rendering pipeline. These methods pass the extracted PDF markdown as `text_content` to `TemplateManager.render_template()`, which substitutes the variable into the Jinja syntax:

```python
from prompts.template_manager import TemplateManager

tm = TemplateManager()
resume_md = """John Doe\njohn@example.com\n..."""

# Render the template with injected content

prompt = tm.render_template("basics", text_content=resume_md)

```

### Step 3: Constructing the LLM Request

The `_call_llm_for_section` method (lines 70-94 in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py)) assembles the final API payload using two rendered components:

- **System message**: Rendered from `system_message.jinja`, providing generic extraction context and section identifiers.
- **User message**: The section-specific template output containing the resume markdown wrapped in instructional text.

This dual-template approach maintains consistent system behavior while allowing section-specific customization of user prompts.

### Step 4: JSON Extraction and Validation

Each template contains explicit instructions demanding the LLM "return ONLY a JSON object" followed by the exact schema. The `_extract_all_sections_separately` method (lines 66-100) receives this output, validates it against Pydantic models, and aggregates the results into a `JSONResume` object. This pipeline ensures that unstructured PDF text transforms into type-safe Python objects.

## Implementing Jinja-Based Prompt Engineering

### Rendering a Section Template

The following example demonstrates how to manually render a prompt for the "basics" section:

```python
from prompts.template_manager import TemplateManager

tm = TemplateManager()
resume_markdown = """Jane Smith\njane@example.com\n San Francisco, CA"""

# Inject resume content into the Jinja template

rendered_prompt = tm.render_template("basics", text_content=resume_markdown)

# Output is ready for OpenAI/Anthropic API

print(rendered_prompt)

```

### Full Pipeline Execution

For end-to-end processing, the `PDFHandler` class orchestrates template rendering and LLM communication:

```python
from pdf import PDFHandler

handler = PDFHandler()
pdf_path = "candidate_resume.pdf"

# Extract all sections using Jinja prompts

structured_resume = handler.extract_json_from_pdf(pdf_path)

# Access typed attributes

print(structured_resume.basics.name)
print(structured_resume.work[0].company)

```

### Anatomy of a Prompt Template

The `basics.jinja` file illustrates how prompt engineering combines static instructions with dynamic variables:

```jinja
Extract ONLY the basic information (name, email, phone, location, profiles) from this resume.

--- The input resume markdown starts here ---
{{ text_content }}
--- The input resume markdown ends here ---

Return ONLY a JSON object with this structure:
{
  "basics": {
    "name": "Full name",
    "email": "Email address",
    "phone": "Phone number",
    "url": null,
    "summary": null,
    "location": { "city": "City", "countryCode": "Country code" },
    "profiles": [
      { "network": "Platform name", "url": "Full URL", "username": "Username from URL" }
    ]
  }
}

```

## Why Jinja Templates for Prompt Engineering?

**Separation of Concerns**: By storing prompt templates in `prompts/templates/` rather than embedding them as Python f-strings or constants, the repository allows prompt engineers to modify extraction instructions without touching [`template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/template_manager.py) or [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py). This decoupling enables version control of prompts independent of application logic.

**Dynamic Variable Injection**: The `{{ text_content }}` syntax allows the same template to process thousands of distinct resumes while maintaining consistent instruction framing. Additional parameters—such as section names or formatting hints—can be passed via `**kwargs` to `render_template()`.

**Reusability and Consistency**: The `system_message.jinja` template is reused across all section extractions, ensuring uniform tone and context, while individual templates (`work.jinja`, `education.jinja`, etc.) tailor the specific extraction requirements. This pattern eliminates duplication and reduces maintenance overhead.

## Summary

- The **TemplateManager** class in [`prompts/template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/prompts/template_manager.py) initializes a Jinja environment targeting the `prompts/templates` directory and caches all `.jinja` files at startup.
- **Seven distinct templates** handle specific resume sections: `basics.jinja`, `work.jinja`, `education.jinja`, `skills.jinja`, `projects.jinja`, `awards.jinja`, plus the reusable `system_message.jinja`.
- Raw resume markdown is injected via the `text_content` variable during calls to `TemplateManager.render_template()`, which is invoked from [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) methods matching `extract_*_section`.
- The architecture separates **system context** (from `system_message.jinja`) from **section-specific user prompts**, combining both in `_call_llm_for_section` to form complete LLM requests.
- Returned content is validated against Pydantic models and assembled into a `JSONResume` object by `_extract_all_sections_separately`, ensuring type-safe structured data extraction.

## Frequently Asked Questions

### What is the role of TemplateManager in the Hiring-Agent repository?

The **TemplateManager** acts as a centralized factory for Jinja template rendering. It initializes the Jinja `Environment`, discovers template files in `prompts/templates/`, and exposes `render_template(section, **kwargs)` to inject variables like `text_content` into prompt schemas. This abstraction allows [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) to request fully-formed prompts without handling file I/O or template syntax directly.

### How does the repository ensure the LLM returns valid JSON?

Each Jinja template incorporates explicit prompt engineering techniques, including the mandatory instruction "Return ONLY a JSON object" followed by the complete expected schema. By embedding the JSON structure directly within `basics.jinja`, `work.jinja`, and other templates, the system constrains the LLM output to deterministic, parseable formats that downstream Pydantic validators can reliably process.

### Can prompts be modified without changing Python code?

Yes. Because **Jinja templates** reside as separate files in `prompts/templates/`, contributors can adjust wording, add few-shot examples, or modify JSON schema requirements using any text editor. These changes take effect immediately upon application restart without requiring modifications to [`template_manager.py`](https://github.com/interviewstreet/hiring-agent/blob/main/template_manager.py) or [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py), facilitating rapid A/B testing of prompt strategies.

### Which resume sections use dedicated Jinja templates?

The repository maintains individual templates for **basics**, **work**, **education**, **skills**, **projects**, and **awards**, plus a **system_message** template reused across all extractions. Each corresponds to specific extraction methods in [`pdf.py`](https://github.com/interviewstreet/hiring-agent/blob/main/pdf.py) (such as `extract_basics_section` and `extract_work_section`), allowing tailored prompts that account for the unique data structures of employment history versus educational credentials.