How to Create Custom Jinja Templates for Prompt Engineering in the Hiring Agent

Create a Jinja file in prompts/templates/, register it in TemplateManager._load_templates, and call render_template() to inject dynamic data into LLM prompts.

The interviewstreet/hiring-agent repository uses Jinja2 to manage LLM prompts for resume parsing tasks. By creating custom Jinja templates for prompt engineering, you can define new extraction sections without modifying the core Python logic. The system loads templates from disk, caches them in a Jinja Environment, and renders them with variables like {{ text_content }} at runtime.

Architecture of the Template System

The Hiring Agent separates prompt content from execution logic through a centralized template manager. Understanding this architecture is essential before extending it with custom templates.

Core Components

  • TemplateManager (prompts/template_manager.py): Scans the prompts/templates directory, initializes a Jinja Environment with FileSystemLoader, and caches templates in memory. It exposes render_template(section_name, **kwargs) to generate final prompt strings.

  • Template Files: Plain Jinja files (e.g., basics.jinja, work.jinja) stored in prompts/templates/. These files define system messages or user prompts using placeholders like {{ text_content }}.

  • PDFHandler (pdf.py): Consumes rendered templates by calling TemplateManager.render_template, then passes the result to the LLM provider via _call_llm_for_section.

The rendering flow follows four stages:

  1. Load: TemplateManager.__init__ creates the Jinja Environment pointing to prompts/templates.
  2. Cache: _load_templates iterates over the template_files dictionary and stores each Template object in self._templates.
  3. Render: render_template(section, **kwargs) retrieves the cached template, injects variables, and returns the prompt string.
  4. Consume: Callers like PDFHandler request specific sections and forward rendered prompts to the LLM.

Step-by-Step Guide to Creating Custom Jinja Templates

Step 1 – Create the Template File

Add a new Jinja file to prompts/templates/. The filename must match the entry you will register in the next step.

You are a senior hiring engineer. Extract the following fields from the provided resume markdown:
- leadership experience
- patents (if any)

--- Resume starts ---
{{ text_content }}
--- Resume ends ---

Return ONLY a JSON object:
{
  "custom": {
    "leadership": {{ ""|tojson }},
    "patents": {{ ""|tojson }}
  }
}

The {{ text_content }} placeholder receives the raw resume markdown. You can define additional variables such as {{ candidate_name }} or {{ job_role }} and pass them during rendering.

Step 2 – Register the Template

Edit TemplateManager._load_templates in prompts/template_manager.py to include your new file in the template_files dictionary:

template_files = {
    # … existing entries …

    "custom_section": "custom_section.jinja",
}

The dictionary key ("custom_section") becomes the identifier used when calling render_template.

Step 3 – Render from Your Code

Import TemplateManager and invoke render_template with your section key and required variables:

from prompts.template_manager import TemplateManager

tm = TemplateManager()
prompt = tm.render_template(
    "custom_section",
    text_content=resume_markdown,
    candidate_name="Alice"  # optional custom variable

)

The method returns a fully rendered string ready for the LLM.

Step 4 – Send to LLM

Follow the pattern in PDFHandler._call_llm_for_section to send the rendered prompt:

chat_params = {
    "model": DEFAULT_MODEL,
    "messages": [
        {"role": "system", "content": prompt},
    ],
    "options": {"temperature": 0.1, "top_p": 0.9},
}
response = provider.chat(**chat_params)

Parse the JSON response using the existing extract_json_from_response utility and map it to a Pydantic model (e.g., CustomSectionModel).

Complete Working Example

Here is a full implementation of a custom template for extracting leadership experience:

File: prompts/templates/leadership.jinja

You are an AI recruiter specializing in engineering leadership. Analyze the resume below and extract:
1. Team sizes managed
2. Duration in leadership roles
3. Notable achievements

Resume Content:
{{ text_content }}

Return ONLY valid JSON matching this structure:
{
  "leadership": {
    "team_sizes": [],
    "durations": [],
    "achievements": []
  }
}

Registration: Update prompts/template_manager.py

class TemplateManager:
    def _load_templates(self):
        template_files = {
            "basics": "basics.jinja",
            "work": "work.jinja",
            "leadership": "leadership.jinja",  # new entry

        }
        # ... existing loading logic

Usage: Extend PDFHandler or create a standalone caller

def extract_leadership(self, resume_text: str) -> Optional[Dict]:
    prompt = self.template_manager.render_template(
        "leadership", text_content=resume_text
    )
    if not prompt:
        logger.error("Failed to render leadership template")
        return None
    return self._call_llm_for_section(
        "leadership", resume_text, prompt, LeadershipModel
    )

Summary

  • The TemplateManager in prompts/template_manager.py orchestrates Jinja template loading and rendering for all LLM interactions.
  • Create new templates as .jinja files in prompts/templates/ using standard Jinja2 syntax and placeholders like {{ text_content }}.
  • Register each template in the template_files dictionary within _load_templates to enable caching and retrieval.
  • Call render_template(section_name, **kwargs) to inject dynamic data and generate prompts for the LLM provider.
  • Reference PDFHandler._call_llm_for_section in pdf.py for the standard pattern of sending rendered prompts and parsing JSON responses.

Frequently Asked Questions

How does the TemplateManager cache templates for performance?

During initialization, TemplateManager.__init__ invokes _load_templates, which iterates over the template_files dictionary and pre-compiles each Jinja file into a Template object stored in self._templates. This eliminates file I/O on subsequent calls to render_template, reducing latency when processing multiple resumes.

Can I pass custom variables beyond text_content to a template?

Yes. The render_template method accepts arbitrary keyword arguments via **kwargs. Define placeholders in your Jinja file (e.g., {{ candidate_name }} or {{ job_id }}), then pass them when rendering: tm.render_template("custom_section", text_content=resume, candidate_name="Bob"). All kwargs are injected into the template context automatically.

Where should I place new template files in the repository?

Place all custom Jinja templates in the prompts/templates/ directory at the repository root. The TemplateManager initializes its Jinja Environment with FileSystemLoader("prompts/templates"), so files outside this path will not be discovered or loaded during the caching phase.

How do I validate the JSON output from custom templates?

Follow the existing pattern in pdf.py where extract_json_from_response parses the LLM's string output into a Python dictionary. Define a Pydantic model (e.g., CustomSectionModel) that matches your template's JSON schema, then validate the parsed data against this model before using it in your application logic.

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 →