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

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 or template_manager.py.

TemplateManager: The Rendering Engine

At the heart of the system lies the TemplateManager class defined in 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, 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:

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) 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:

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:

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:

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 or 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 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 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 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 or 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 (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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →