---
audience: designers, developers
summary: TEMPLATE_FAILED errors and how to diagnose them.
title: Template errors
path: troubleshooting/templates
status: published
---


# Template troubleshooting

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](/docs/scaiscribe/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:

```
{{ 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:

```
{{ 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:

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

vs.

```
{{ 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.

```
| 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:

```
{{ 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.

**Cause** — `HTML` 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:

```
{{ 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"

**Symptom** — `POST /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
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](/docs/scaiscribe/reference/templates#expression-language-pptxxlsx-safe-subset) 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

- [Templates reference](/docs/scaiscribe/reference/templates) — complete grammar.
- [Tutorials → Templates](/docs/scaiscribe/tutorials/templates) — end-to-end example.
- [Concepts → Templates](/docs/scaiscribe/concepts/templates) — the model.
