Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/odoo/documentation/llms.txt

Use this file to discover all available pages before exploring further.

QWeb is Odoo’s primary XML templating engine, used for generating HTML fragments in both server-side Python controllers and client-side OWL components. Template directives are XML attributes prefixed with t-. A <t> placeholder element executes its directive without emitting any HTML tag itself.
<!-- <t> is transparent — only its content is rendered -->
<t t-if="condition">
  <p>Test</p>
</t>
<!-- renders: <p>Test</p> when condition is truthy, nothing otherwise -->

<!-- a real element renders itself -->
<div t-if="condition">
  <p>Test</p>
</div>
<!-- renders: <div><p>Test</p></div> -->

Data Output

t-out (safe output)

t-out HTML-escapes its value by default, protecting against XSS:
<p><t t-out="value"/></p>
<!-- with value=42 → <p>42</p> -->
Content that is already marked safe (e.g. markupsafe.Markup in Python) is injected as-is without re-escaping.

Deprecated Output Directives

DirectiveStatusNotes
t-escLegacy aliasIdentical to t-out; not yet formally deprecated
t-rawDeprecated since 15.0Never escapes; use t-out with Markup instead
t-raw was deprecated because it is easy to inadvertently inject unsanitized HTML as the code producing the value evolves. Replace all uses of t-raw with t-out + a markupsafe.Markup value on the Python side.

Conditionals

<div>
  <p t-if="user.birthday == today()">Happy birthday!</p>
  <p t-elif="user.login == 'root'">Welcome master!</p>
  <p t-else="">Welcome!</p>
</div>
The directive can be placed on any element — it controls whether that element is included in the output.

Loops

t-foreach iterates over arrays, mappings, or integers (deprecated). Pair it with t-as to name the iteration variable:
<t t-foreach="[1, 2, 3]" t-as="i">
  <p><t t-out="i"/></p>
</t>
Inside the loop body, QWeb provides automatic helper variables. Replace $as with your t-as name:
VariableDescription
$as_valueCurrent item value (same as $as for lists; for dicts it’s the value, $as is the key)
$as_indexZero-based iteration index
$as_sizeTotal size of the collection (if available)
$as_firsttrue on the first iteration
$as_lasttrue on the last iteration (requires $as_size)
$as_allThe entire collection (JS only, deprecated)
$as_parity"even" or "odd" (deprecated)
$as_evenBoolean flag for even index (deprecated)
$as_oddBoolean flag for odd index (deprecated)
Variables created inside a t-foreach scope are local to that loop, except for variables that already existed in the outer scope — those are updated in the outer scope after the loop ends.
<t t-set="existing_variable" t-value="False"/>
<p t-foreach="[1, 2, 3]" t-as="i">
  <t t-set="existing_variable" t-value="True"/>
  <!-- new_variable is local to the loop -->
  <t t-set="new_variable" t-value="True"/>
</p>
<!-- existing_variable is now True, new_variable is undefined -->

Attributes

Three forms of dynamic attribute generation:
Evaluates an expression and sets it as the named attribute:
<div t-att-a="42"/>
<!-- → <div a="42"></div> -->

Setting Variables

t-set creates a template-local variable:
<!-- Using t-value expression -->
<t t-set="foo" t-value="2 + 1"/>
<t t-out="foo"/>
<!-- → 3 -->

<!-- Using node body as value (renders body and assigns result) -->
<t t-set="foo">
  <li>ok</li>
</t>
<t t-out="foo"/>

Sub-Templates

t-call invokes another named template within the current execution context:
<t t-call="other-template"/>
Variables set inside the call body are local to that call (they don’t leak to the parent):
<t t-call="other-template">
  <t t-set="var" t-value="1"/>
</t>
<!-- "var" does not exist here -->
The rendered body of the call is available inside the sub-template as the magic variable 0:
<!-- other-template definition -->
<div>
  Content passed:
  <t t-out="0"/>
</div>

<!-- calling it -->
<t t-call="other-template">
  <em>content</em>
</t>
<!-- → <div>Content passed: <em>content</em></div> -->

Advanced Output (Python-side)

In Python QWeb, the following produce safe (non-re-escaped) content:
  • odoo.fields.Html field values
  • html_escape() / markupsafe.escape() — escape a string and mark it safe
  • html_sanitize() — sanitize and mark safe
  • markupsafe.Markup(...) — explicit safe assertion (use with care)
To force double-escaping of already-safe content: str(content) in Python, String(content) in JavaScript.

Python-Exclusive Directives

t-field (Smart Record Fields)

t-field formats a model field value using its type-specific widget. Only valid on browse() record attributes:
<span t-field="record.name"/>
<span t-field="record.date" t-options="{'widget': 'date'}"/>
Use t-options to pass widget-specific options.

Request-Based Rendering

In HTTP controllers, render a QWeb view template:
response = http.request.render("my-template", {
    "context_value": 42
})
The _render method on ir.qweb renders by database ID or external ID:
html = self.env["ir.qweb"]._render("addon_name.template_xml_id", values)

t-debug

Invokes breakpoint() (Python debugger) during rendering:
<t t-debug=""/>

JavaScript-Exclusive Directives

t-name (Define a Template)

<templates>
  <t t-name="myaddon.MyComponent">
    <!-- template code -->
  </t>
</templates>
All template files registered in asset bundles are loaded into the OWL engine automatically.

Template Inheritance

Two modes of template inheritance:
Creates a new template derived from a parent. Use t-inherit-mode="primary" and provide a new t-name:
<t t-name="myaddon.MyView"
   t-inherit="web.KanbanView"
   t-inherit-mode="primary">
  <xpath expr="//Layout" position="before">
    <div>Extra ribbon</div>
  </xpath>
</t>
XPath position values: before, after, inside (append), replace, attributes.

JavaScript Debugging Hooks

DirectiveDescription
t-log="expr"console.log(expr) during rendering
t-debug=""Triggers a debugger breakpoint in the browser
t-js="ctx"Executes inline JS with ctx bound to the render context
<t t-set="foo" t-value="42"/>
<t t-log="foo"/>
<!-- → logs 42 to the browser console -->

<t t-js="ctx">
  console.log("foo is", ctx.foo);
</t>

QWeb2.Engine API (Client-Side)

Access the global QWeb instance via core.qweb (from the legacy web.core module):
// Render a template to an HTML string
const html = core.qweb.render("myaddon.MyTemplate", { name: "World" });
Key QWeb2.Engine methods:
MethodDescription
render(template, context)Renders a loaded template to a string
add_template(templates)Loads templates from an XML string, URL, or DOM node

Build docs developers (and LLMs) love