Skip to content

Templates

Wherever a Pro tool lets you shape the review page with your own markup, the template is LiquidJS, rendered by the same engine with the same rules: elicit_approval’s displayTemplate, elicit_proposal’s displayTemplate (a header above the diff grid), and elicit_selection’s rowTemplate / headingTemplate / footerTemplate / hoverTemplate. This page is that shared contract; each tool’s page covers the specifics of what it renders and where.

Templates are LiquidJS (the JavaScript implementation of Shopify’s Liquid). You get the usual Liquid toolkit — {{ output }}, {% if %} / {% unless %} / {% for %}, filters like {{ x | default: "…" }} and {{ x | upcase }} — evaluated against a context the tool provides: {{ context.* }} for approval and proposal payloads, {{ option.* }} for selection rows.

Templates travel to the API as JSON strings, and JSON has no multi-line string syntax — but you never need to hand-write a template as one long line. Author it as a multi-line string in your language (a JS/TS template literal, a Python triple-quoted string) and let JSON serialization handle the escaping:

const displayTemplate = `
<strong>Deploy {{ context.service }} {{ context.version }}</strong>
<table>
{% for c in context.changes %}
<tr><td>{{ c.file }}</td><td>+{{ c.added }}</td><td>−{{ c.removed }}</td></tr>
{% endfor %}
</table>
`

Newlines and indentation inside a template are legal and insignificant — the output is HTML, where whitespace collapses. (If a stray newline ever matters, Liquid’s whitespace control tags {%- -%} trim it.) For long templates, keep the template in its own file in your repo and read it in when building the call.

We run LiquidJS with its jsTruthy option enabled, so {% if %}, {% unless %}, and and/or follow JavaScript truthiness:

Value Truthy?
false, null, undefined falsy
0 falsy
"" (empty string) falsy
[] (empty array), {} (empty object) truthy
any non-empty string, non-zero number, populated array/object truthy

So {% if option.count %} is what a JavaScript developer expects — 0 skips the block, an empty string skips the block, a missing field skips the block:

{% if option.logo %}<img src="{{ option.logo }}">{% endif %}
{% if option.discount %}Save {{ option.discount }}%{% endif %} {# 0 → nothing #}

This matches JavaScript and Nunjucks. It deliberately differs from Shopify’s default Liquid (where 0 and "" are truthy — a common source of surprise). One thing to know: an empty array is truthy here (as in JavaScript and Nunjucks — but unlike Jinja2/Python, where empty collections are falsy). To branch on whether a list has items, check its size explicitly rather than the array itself:

{% if option.tags.size > 0 %}...{% endif %} {# not: {% if option.tags %} #}

The default filter follows the same truthiness, so a value of 0 (as well as null, "", or a missing field) triggers the fallback:

{{ option.label | default: option.id }} {# empty/missing label → falls back to id #}

Interpolated data ({{ context.… }}, {{ option.… }}) is HTML-escaped, so payload or option content can’t break the layout or inject markup — your template’s own tags render as-is, but the values dropped into them are inert text. Templates are rendered server-side and their output is displayed inside a sandboxed, null-origin iframe. The exact sandbox differs by flow — the selection table runs with no scripting (sandbox="allow-forms") so its native radio/checkbox controls can’t be spoofed, while approval and proposal display templates are display-only (sandbox="allow-scripts", no same-origin access) — see Selection Flow and elicit_approval for the details.

  • Size: each template is capped at 20 KB.
  • Cost: rendering runs under parse-length, render-time, and memory caps; there is no access to the network, the filesystem, or anything outside the context you pass.
  • Own-property only: templates read your data’s own properties, not inherited or prototype fields.
  • Fallback: if a template fails to parse or render, the page falls back to a safe default (the pretty-printed JSON for approvals, a plain id + label table for selections, no header above the grid for proposals) — a bad template never blocks a request.