Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Template errors

Most template errors come from one of three places: a typo in the template grammar, a missing variable in the payload, or expecting a feature ScaiScribe doesn't expose. This page covers each in order.

For the complete grammar reference, see Reference → Templates.

TEMPLATE_FAILED: Could not find … in path "X"#

Symptom — Instantiation returns 500 with this code; the message names the missing path.

Cause — The template references a variable that wasn't in the variables payload of POST /v1/documents/from_template.

Fix — Add the variable. Use the placeholders field from POST /v1/templates as a checklist; every literal name in there (filtered of FOR/IF/ALIAS keywords and $-prefixed loop vars) needs to be present.

If the variable is optionally present, defend in the template:

django
1
2
{{ recipient.name ?? 'Sir/Madam' }}
{{ subscriber?.tier ?? 'free' }}

Optional chaining (?.) and nullish coalescing (??) work because the expression is JavaScript.

TEMPLATE_FAILED: Unexpected token …#

Symptom — Instantiation returns 500; the message names a JS syntax error.

Cause — Invalid JavaScript inside {{ … }}. The most common mistake is double curly braces meant as object literals inside the command:

django
1
2
3
{{ LINK ({ url: project.url, label: project.name }) }}
                                                   ^  ← This } is the LINK closer.
                                                       The earlier } closes the object literal.

Tools like Word's autocorrect sometimes mangle quotes (' becomes ', breaking JS). Re-type quotes; if your IDE does smart quotes, disable them for the template doc.

Fix — Paste the expression into a JS console; fix the syntax. Keep template logic simple — anything beyond one chained method call belongs in your application code preparing the variables, not in the template.

TEMPLATE_FAILED: Unmatched END-FOR / Unmatched END-IF#

Symptom — Instantiation returns 500 with an unmatched-block message.

Cause — A FOR or IF opening command is missing its closing partner. Common culprits:

  • Typo in the loop variable name ({{ FOR p IN people }}{{ END-FOR person }} — the names must match exactly).
  • The opening command got deleted accidentally while editing the template.
  • The opening command is inside a different table cell than END-FOR, and Word's autoflow split them across pages confusingly.

Fix — Inspect the template in Word's raw view (Alt-F9 cycles field visibility). Match openings to closings.

For IF: END-IF takes no parameter ({{ END-IF }}). For FOR: END-FOR name takes the same name as the opening ({{ END-FOR person }}).

Loop body produces no output#

Symptom{{ FOR x IN items }}…{{ END-FOR x }} renders an empty section in the output even though items has entries.

Cause — Most likely you forgot the $ prefix on the loop variable inside the body:

mysql
1
2
3
{{ FOR person IN people }}
{{ person.name }}                   WRONG: refers to outer `person`, which is undefined
{{ END-FOR person }}

vs.

django
1
2
3
{{ FOR person IN people }}
{{ $person.name }}                 ← Correct: $-prefix is required inside the loop body
{{ END-FOR person }}

Fix — Add the $ prefix everywhere inside the loop body, including in nested expressions and inside ALIAS definitions.

Loop replicates the wrong rows in a table#

Symptom{{ FOR }} markers placed inside a table cell, but the loop replicates the wrong row count (or doesn't replicate at all).

Cause — Row-loop placement is row-level. FOR goes in a row BY ITSELF above the data row. END-FOR goes in a row BY ITSELF below.

django
1
2
3
4
5
| Name              | Since              |     ← header row (not replicated)
|-------------------|--------------------|
| {{ FOR p IN people }}                  |     ← marker row (removed at render)
| {{ $p.name }}     | {{ $p.since }}     |     ← data row (replicated)
| {{ END-FOR p }}                        |     ← marker row (removed at render)

If FOR shares a row with cells holding actual content, docx-templates won't treat it as a row-replication marker — it'll treat it as an inline marker, producing surprising output.

Fix — Put FOR and END-FOR in their own dedicated rows.

IMAGE command silently fails / produces no image#

Symptom{{ IMAGE … }} renders as either an empty space or the literal text of the command.

Cause — Most likely the expression references a helper function (like qrCode(url)) that doesn't exist. ScaiScribe does not pass additionalJsContext to docx-templates, so any function not on global JS (Math, Date, Array, etc.) isn't available.

Fix — Pre-render the image bytes in your application; pass the base64 string in variables; reference it directly:

gdscript
1
{{ IMAGE ({ width: 5, height: 5, data: variables.logo_b64, extension: '.png' }) }}

variables.logo_b64 must be a base64-encoded string of the PNG/JPG/SVG bytes. Width/height are in centimetres.

HTML command renders as a placeholder#

Symptom{{ HTML … }} renders correctly in Word but appears as a "this content can't be displayed" placeholder in LibreOffice or Google Docs.

CauseHTML uses Word's altchunk feature, which is Word-specific. Other Office clients don't support it.

Fix — Either commit to Word as the consumer, or rewrite the HTML content as normal docx structure (multiple commands, regular paragraph formatting, etc.).

"Why isn't numberFormat working?"#

Symptom{{ amount | numberFormat: '0,0.00' }} renders as the literal text 0,0.00 or throws a syntax error.

Cause — There is no numberFormat filter. ScaiScribe doesn't pass a numeral.js integration or any other filter library. Commands are pure JavaScript expressions.

Fix — Use JS built-ins:

django
1
2
3
4
5
{{ amount.toFixed(2) }}                                          ← 1234.50
{{ amount.toLocaleString('de-DE', {minimumFractionDigits: 2}) }} ← 1.234,50
{{ '€' + amount.toLocaleString('en-IE') }}                       ← €1,234.5
{{ new Intl.NumberFormat('nl-NL', {style: 'currency', currency: 'EUR'}).format(amount) }}
                                                                 ← € 1.234,50

For dates: new Date(deadline).toLocaleDateString('en-GB').

The placeholder list in the API response has too many "placeholders"#

SymptomPOST /v1/templates returns a placeholders array containing entries like "FOR item IN items", "$item.name", "END-FOR item".

Cause — ScaiScribe's placeholder scanner is a regex that finds every {{ … }} token. It doesn't distinguish variables from commands. This is by design — the list is a structural inventory, not a variable catalogue.

Fix — In your operator UI, filter the list:

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
keywords = {"INS", "FOR", "END-FOR", "IF", "END-IF", "ALIAS", "EXEC", "IMAGE", "LINK", "HTML"}

def is_variable(p):
    parts = p.strip().split()
    if not parts: return False
    if parts[0] in keywords: return False
    if parts[0].startswith("="): return False
    if parts[0].startswith("$"): return False     # loop variable
    if parts[0].startswith("*"): return False     # alias reference
    return True

variables = [p for p in template.placeholders if is_variable(p)]

The remaining list is the variables your variables payload needs to provide.

Placeholders in headers/footers don't appear in the API response#

Symptom — Your template has a {{ company_name }} in the header. It renders correctly at instantiation time, but it's not in the placeholders API response.

Cause — The placeholder extractor scans only word/document.xml. Headers, footers, and other zip parts aren't scanned. Instantiation processes them all correctly via docx-templates — the gap is only in the API's placeholders field.

Fix — Maintain a manual list of header/footer variables if your operator UI depends on the API list. Or accept that the API list undercounts; the call still works.

PPTX / XLSX-specific issues#

{{ var }} survives unchanged in the output#

Symptom — A {{ name }} token still appears verbatim in the rendered PPTX/XLSX.

Cause — The token got split across multiple OOXML runs in a way the coalescer couldn't reconstruct. Most common when:

  1. The template was authored by pasting placeholders from another app (Word, browser, chat) — paste tends to inject formatting markers between characters, splitting the token across many runs.
  2. PowerPoint's spell-check underlined "{{" or "}}" — the underline becomes a separate run.
  3. Mid-token formatting changes (e.g. you bolded just the name part inside {{ name }}).

Fix — Re-type the placeholders directly in PowerPoint/Calc. If the issue persists, save the file, unzip it, inspect ppt/slides/slide1.xml (or xl/sharedStrings.xml), and look for the affected <a:r> / <t> chain. Combining the runs manually OR re-authoring that text frame from scratch fixes it.

Missing variables produce empty space instead of an error#

Symptom{{ missing }} renders as an empty cell or empty text frame in the output; no error.

Cause — By design. PPTX and XLSX templates are often partially-filled (different teams contribute different parts). The Tier 1 engine renders missing paths as empty strings and emits a TEMPLATE_VARIABLE_MISSING warning in the result payload's warnings[] array.

Fix — Inspect the warnings field of the from_template response. If you want stricter behaviour, validate the variables payload against the API's placeholders list before calling from_template.

{{ price.toFixed(2) }} renders as empty + TEMPLATE_EXPRESSION_ERROR warning#

Symptom — An expression like {{ amount.toFixed(2) }} renders as empty cell/text in PPTX/XLSX; the result payload's warnings[] carries a TEMPLATE_EXPRESSION_ERROR with the expression text.

Cause — One of three things:

  1. The variable doesn't resolve (e.g. amount is null/undefined in the payload).
  2. The method isn't on the per-type allowlist (e.g. count.constructor, name.eval(…)).
  3. The expression has a syntax error or uses an unsupported construct (arrow functions, new, etc.).

Fix — Check the warning's message field for the parser/evaluator error. Common fixes:

  • Pass the variable. Use the API's placeholders list to validate your payload upfront.
  • Use a method from the allowlist (see the reference for the per-type catalogue).
  • Simplify the expression — Tier 2 supports method chains + arithmetic + comparisons + ternaries, but not arrow functions, new, or arbitrary globals.

If you need a feature that's not in the safe subset, pre-format the value in your application code and pass it as a plain string.

{{ FOR person IN people }} markers stay literal in the output#

Symptom — FOR markers survive in the output as visible text instead of triggering replication.

Cause — One of three structural issues:

  1. The marker isn't on its own paragraph (PPTX) or row (XLSX). The marker must be the only non-whitespace content in its containing paragraph/row. A paragraph like Header: {{ FOR x IN list }} is treated as substitution + literal text — not as a loop trigger.
  2. Missing END-FOR. Without a matching END-FOR (in a separate paragraph/row), the FOR is silently ignored. Check the name — {{ END-FOR person }} matches {{ FOR person IN ... }}; the name on END-FOR is optional but if present must match.
  3. Slide-level FOR placement. For whole-slide replication, the FOR marker must be the very first text content on the slide and END-FOR the very last. Any other text appearing before the FOR or after the END-FOR makes the slide a regular substitution slide instead.

Fix — Inspect the template's structure. For PPTX, ensure marker paragraphs are alone in their <a:p>. For XLSX, ensure marker cells are in their own row. For slide-level FOR, the FOR/END-FOR text should be the first/last text in the slide.

Formula cell references break after row replication#

Symptom — An XLSX template has =SUM(B2:B4) in a totals row; after instantiation with 3 items the formula still says B2:B4 but the data now lives in B2:B4 correctly… or, worse, the data lives in B2:B6 but the formula still says B2:B4.

Cause — The formula's row refs are inside the FOR body: they got rewritten on each replica. The totals row outside the loop kept its original refs, which may or may not be correct after row renumbering depending on where it sits.

Fix — Anchor totals formulas to absolute row refs (=SUM($B$2:$B$4)) — those don't get rewritten. Or compute the total in your application code and pass it as a variable.

The cell-ref rewriter only touches relative refs whose row number exactly matches the row's source row number. Refs to other rows (e.g. a totals formula referring to data rows) survive untouched, which is sometimes what you want and sometimes not.

numberFormat filter / piping doesn't work in PPTX/XLSX#

Same answer as the DOCX numberFormat question above — but in PPTX/XLSX the Tier 2 engine does support JS-style method calls. Format inline with {{ amount.toFixed(2) }} or {{ amount.toLocaleString("de-DE") }}. For dates, pre-format in application code (there's no Date global).

XLSX cell shows literal text "{{ recipient.name }}" instead of substituting#

Symptom — In a .xlsx template you typed {{ recipient.name }} into a cell. After instantiation the cell still shows the literal text.

Cause — Some XLSX writers (notably certain LibreOffice configurations and ancient Excel) store cell text as a formula returning a string rather than as a shared/inline string. The engine doesn't substitute inside formula bodies.

Fix — Make sure the cell value is a literal string, not a formula. In Excel: select the cell, the formula bar should show {{ recipient.name }} without a leading =. If it shows ="{{ recipient.name }}", delete and re-type without the equals sign.

Where to next#

Updated 2026-07-03 13:00:10 View source (.md) rev 9