> Use when writing Jinja templates for ERPNext/Frappe Print Formats, Email Templates, and Portal Pages. Covers template syntax, context variables, filters, macros, and v16 Chrome PDF rendering. Prevents common mistakes email template, portal page, template syntax, PDF, v14-v16, template syntax, Jinja example, print format code, how to show child table in print.
npx skills add https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-syntax-jinja
> Deterministic Jinja reference for Print Formats, Email Templates, Notification Templates, and Portal Pages in Frappe v14/v15/v16.
USE when:
www/*.html) with Python controllershooks.pyDO NOT USE for:
{%= %}), NOT Jinjafrappe-syntax-clientscriptsfrappe-syntax-serverscriptsNeed a printable document?
├─ YES → Is it for a Query/Script Report?
│ ├─ YES → Use JS Template ({%= %}), NOT Jinja
│ └─ NO → Use Jinja Print Format
└─ NO → Is it for email?
├─ YES → Is it triggered by workflow/notification?
│ ├─ YES → Notification Template (Jinja)
│ └─ NO → Email Template (Jinja)
└─ NO → Is it a web page?
├─ YES → Portal Page (www/*.html + .py controller)
└─ NO → frappe.render_template() for ad-hoc rendering
| Syntax | Purpose | Example |
|--------|---------|---------|
| {{ }} | Output expression | {{ doc.name }} |
| {% %} | Control statement | {% if doc.status == "Paid" %} |
| {# #} | Comment | {# This is a comment #} |
| {{ _("text") }} | Translation | {{ _("Invoice") }} |
| {{ val \| filter }} | Filter | {{ name \| default("N/A") }} |
| Aspect | Jinja (Print Formats) | JS Template (Report Print Formats) |
|--------|----------------------|-------------------------------------|
| Output | {{ expression }} | {%= expression %} |
| Code block | {% statement %} | {% js_code %} |
| Language | Python | JavaScript |
| Context | doc, frappe | data, filters |
NEVER use Jinja syntax in Report Print Formats. NEVER use {%= %} in standard Print Formats.
| Object | Description |
|--------|-------------|
| doc | The document being printed (full Document object) |
| frappe | Frappe module (whitelisted methods only) |
| frappe.utils | Utility functions |
| _() | Translation function |
| doc.items, doc.taxes | Child table accessors (by fieldname) |
| Object | Description |
|--------|-------------|
| doc | The linked document (when triggered from a DocType) |
| frappe | Frappe module (limited) |
| _() | Translation function |
| Object | Description |
|--------|-------------|
| doc | The document that triggered the notification |
| frappe | Frappe module |
| _() | Translation function |
| Object | Description |
|--------|-------------|
| frappe | Frappe module |
| frappe.session.user | Current authenticated user |
| frappe.form_dict | Query parameters from URL |
| frappe.lang | Current language code |
| Custom context | Set via get_context(context) in .py controller |
> Full details: references/context-objects.md
{# ALWAYS use get_formatted() for fields in Print Formats #}
{{ doc.get_formatted("posting_date") }}
{{ doc.get_formatted("grand_total") }}
{# Child table rows — ALWAYS pass parent doc for currency context #}
{% for row in doc.items %}
{{ row.get_formatted("rate", doc) }}
{{ row.get_formatted("amount", doc) }}
{% endfor %}
{# General formatting with explicit fieldtype #}
{{ frappe.format(value, {'fieldtype': 'Currency'}) }}
{{ frappe.format_date(doc.posting_date) }}
{# Full document — use only when multiple fields needed #}
{% set customer = frappe.get_doc("Customer", doc.customer) %}
{# Single field — ALWAYS prefer over get_doc for one field #}
{% set abbr = frappe.db.get_value("Company", doc.company, "abbr") %}
{# List of records (no permission check) #}
{% set tasks = frappe.get_all("Task",
filters={"status": "Open"},
fields=["title", "due_date"],
order_by="due_date asc",
page_length=10) %}
{# List with permission check (portal pages) #}
{% set orders = frappe.get_list("Sales Order",
filters={"customer": doc.customer},
fields=["name", "grand_total"]) %}
<h1>{{ _("Invoice") }}</h1>
<p>{{ _("Total: {0}").format(doc.get_formatted("grand_total")) }}</p>
{{ frappe.get_url() }}
{{ frappe.get_fullname() }}
{{ frappe.get_fullname(doc.owner) }}
{{ frappe.db.get_single_value("System Settings", "time_zone") }}
{% if frappe.session.user != "Guest" %}...{% endif %}
> Full method reference: references/methods-reference.md
{% if doc.status == "Paid" %}
<span class="paid">{{ _("Paid") }}</span>
{% elif doc.status == "Overdue" %}
<span class="overdue">{{ _("Overdue") }}</span>
{% else %}
<span>{{ doc.status }}</span>
{% endif %}
{% for item in doc.items %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ item.item_name }}</td>
<td>{{ item.get_formatted("amount", doc) }}</td>
</tr>
{% else %}
<tr><td colspan="3">{{ _("No items") }}</td></tr>
{% endfor %}
| Variable | Description |
|----------|-------------|
| loop.index | 1-indexed position |
| loop.index0 | 0-indexed position |
| loop.first | True on first iteration |
| loop.last | True on last iteration |
| loop.length | Total number of items |
{% set total = 0 %}
{% set name = doc.customer_name | default("Unknown") %}
| Filter | Example | Notes |
|--------|---------|-------|
| default | {{ val \| default("N/A") }} | ALWAYS use for optional fields |
| length | {{ items \| length }} | Count items |
| join | {{ names \| join(", ") }} | Join list to string |
| truncate | {{ text \| truncate(100) }} | Truncate with ellipsis |
| escape | {{ input \| escape }} | HTML-escape (default behavior) |
| safe | {{ html \| safe }} | Render raw HTML — NEVER for user input |
| round | {{ num \| round(2) }} | Round number |
| lower / upper | {{ text \| upper }} | Case conversion |
> Full filter reference: references/filters-reference.md
# hooks.py
jenv = {
"methods": [
"myapp.jinja.methods" # Module with callable functions
],
"filters": [
"myapp.jinja.filters" # Module with filter functions
]
}
# myapp/jinja/methods.py
import frappe
def get_company_logo(company):
"""Returns company logo URL. Called as get_company_logo() in Jinja."""
return frappe.db.get_value("Company", company, "company_logo") or ""
<img src="{{ get_company_logo(doc.company) }}" alt="Logo">
# myapp/jinja/filters.py
def nl2br(text):
"""Convert newlines to <br> tags. Used as {{ text | nl2br }}."""
return (text or "").replace("\n", "<br>")
{{ doc.notes | nl2br | safe }}
> Details: references/methods.md
<style>
.print-header { background: #f5f5f5; padding: 15px; }
.item-table { width: 100%; border-collapse: collapse; }
.item-table th, .item-table td { border: 1px solid #ddd; padding: 8px; }
.text-right { text-align: right; }
</style>
<div class="print-header">
<h1>{{ doc.select_print_heading or _("Invoice") }}</h1>
<p>{{ doc.name }} — {{ doc.get_formatted("posting_date") }}</p>
</div>
<table class="item-table">
<thead>
<tr>
<th>#</th>
<th>{{ _("Item") }}</th>
<th class="text-right">{{ _("Qty") }}</th>
<th class="text-right">{{ _("Amount") }}</th>
</tr>
</thead>
<tbody>
{% for row in doc.items %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ row.item_name }}</td>
<td class="text-right">{{ row.qty }}</td>
<td class="text-right">{{ row.get_formatted("amount", doc) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><strong>{{ _("Grand Total") }}: {{ doc.get_formatted("grand_total") }}</strong></p>
/* v14/v15 (wkhtmltopdf) */
.page-break { page-break-before: always; }
/* v16 (Chrome PDF) — ALWAYS prefer break-* in v16 */
.page-break { break-before: page; }
> Full examples: references/examples.md | Patterns: references/patterns.md
| Aspect | v14/v15 (wkhtmltopdf) | v16 (Chrome) |
|--------|----------------------|--------------|
| CSS Support | Limited CSS3 | Full modern CSS |
| Flexbox/Grid | Partial | Full support |
| Page breaks | page-break-* | break-* preferred |
| Fonts | System fonts only | Web fonts supported |
// site_config.json
{
"pdf_engine": "chrome",
"chrome_path": "/usr/bin/chromium"
}
{% extends "templates/web.html" %}
{% block title %}{{ _("Projects") }}{% endblock %}
{% block page_content %}
<h1>{{ _("Projects") }}</h1>
{% for project in projects %}
<h3>{{ project.title }}</h3>
<p>{{ project.description | default("") | truncate(150) }}</p>
{% else %}
<p>{{ _("No projects found.") }}</p>
{% endfor %}
{% endblock %}
import frappe
def get_context(context):
context.title = "Projects"
context.no_cache = True
context.projects = frappe.get_all("Project",
filters={"is_public": 1},
fields=["name", "title", "description"],
order_by="creation desc")
return context
> Full structure: references/structure.md | Templates: references/templates.md
_() for ALL user-facing stringsget_formatted() for currency, date, and numeric fieldsdefault() filter for optional/nullable fieldsdoc to child row get_formatted("field", doc)frappe.db.get_value() when you need only one field| safe on user-supplied input (XSS vulnerability){%= %})frappe.get_doc() when frappe.db.get_value() suffices_() translation wrappersafe_render without security review> Anti-patterns with fixes: references/anti-patterns.md
| File | Contents |
|------|----------|
| references/syntax.md | Jinja syntax reference (tags, filters, tests, loops) |
| references/methods.md | Custom Jinja methods/filters via hooks |
| references/context-objects.md | Available objects per template type |
| references/filters-reference.md | All standard and custom Frappe filters |
| references/methods-reference.md | All frappe.* methods available in Jinja |
| references/examples.md | Complete Print Format, Email, Portal examples |
| references/anti-patterns.md | Common mistakes and correct alternatives |
| references/templates.md | Template structure patterns |
| references/patterns.md | Conditional rendering, loops, child tables |
| references/structure.md | File structure for template types |
frappe-syntax-hooks — jenv configuration in hooks.pyfrappe-impl-printformat — Print Format implementation patternsfrappe-errors-serverscripts — Server-side error handlingConvert Markdown files to HTML similar to `marked.js`, `pandoc`, `gomarkdown/markdown`, or similar tools; or writing custom script to convert markdown to html and/or working on web template systems like `jekyll/jekyll`, `gohugoio/hugo`, or similar web templating systems that utilize markdown documents, converting them to html. Use when asked to "convert markdown to html", "transform md to html", "render markdown", "generate html from markdown", or when working with .md files and/or web a templating system that converts markdown to HTML output. Supports CLI and Node.js workflows with GFM, CommonMark, and standard Markdown flavors.
Write and maintain technical documentation. Trigger with "write docs for", "document this", "create a README", "write a runbook", "onboarding guide", or when the user needs help with any form of technical writing — API docs, architecture docs, or operational runbooks.
Translate visa application documents (images) to English and create a bilingual PDF with original and translation
Write, review, and edit documentation files with consistent structure, tone, and technical accuracy. Use when creating docs, reviewing markdown files, writing READMEs, updating `/docs` directories, or when user says "write documentation", "review this doc", "improve this README", "create a guide", or "edit markdown". Do NOT use for code comments, inline JSDoc, or API reference generation.
Real-time Constitution compliance checker for devflow documents. Blocks partial implementations and hardcoded secrets during file editing.
Write self-documenting code with minimal, evergreen comments that explain complex logic without describing recent changes or temporary fixes. Use this skill when writing code comments, documentation strings, explaining complex algorithms, clarifying business logic, or deciding whether code needs comments. Apply when working with any source code files where comments or documentation might be added, ensuring comments remain relevant, helpful, and focused on explaining why rather than what the code does, while preferring clear code structure and naming over excessive commenting.
Use when writing technical documentation that needs to be readable by both humans and AI models, converting existing docs to HADS format, validating a HADS document, or optimizing documentation for token-efficient AI consumption.
Draft a structured investment committee memo for PE deal approval. Synthesizes due diligence findings, financial analysis, and deal terms into a professional IC-ready document. Use when preparing for investment committee, writing up a deal, or creating a formal recommendation. Triggers on "write IC memo", "investment committee memo", "deal write-up", "prepare IC materials", or "recommendation memo".
Take impertio-studio/frappe-syntax-jinja from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.