16 · XTL function vs Excel formula
Common gotchas — start here
You probably arrived here because something didn't work the way you expected. The most likely cases:
"I want ₩1,234,567 in a cell, TEXT([Amount], "₩#,##0") doesn't work"
XTL's TEXT() ships a small format-token set; currency tokens
aren't in it. The right answer is the cell's number format:
| Step | Where |
|---|---|
1. In the template cell, set the cell format to "₩"#,##0 | Excel's Format Cells dialog → Custom |
2. Put {{ [Amount] }} (the bare number) in that cell | XTL substitution |
The rendered cell holds the number, Excel displays it as
₩1,234,567. Sorting, filtering, and downstream formulas all
keep working because the value is still a number.
The same pattern handles (1,234) accounting-style negatives
(#,##0;(#,##0)), percentages (0.00%), and dates
(yyyy-mm-dd).
"I want =B2*2 to compute per-row, but every row shows the same answer"
xl3 preserves your formula text verbatim across @repeat row
expansion — it does not rewrite B2 into B3, B4, etc.
(See ADR-0046 for the contract.)
Use an XTL expression instead:
{{ [Amount] * 2 }}
This evaluates per row at render time and writes the computed number into each cell. Same result, no row-reference confusion.
"I want a total at the bottom; =SUM(B2:B5) doesn't extend when rows multiply"
Same root cause — xl3 doesn't rewrite range references. Two options:
- Whole-column reference in the footer:
=SUM(B:B)(or use a@filterupstream to keep only data rows). - XTL aggregate: put
{{ SUM([Amount]) }}in the footer cell. This computes at render time and writes the number.
"I want a clickable link per row"
Use the XTL HYPERLINK() function (URL/label can both reference
columns):
{{ HYPERLINK([Url], [Label]) }}
For static URLs, a plain =HYPERLINK("https://...", "label")
formula in the cell also works (xl3 preserves it).
"I want IF(...) with five branches; the nesting is unreadable"
IFS(c1, v1, c2, v2, ...) is the XTL function for multi-branch
conditionals. End with TRUE, default as the fallback:
{{ IFS([R] > 10000, "VIP", [R] > 1000, "Standard", TRUE, "Lite") }}
"I want SUM(Qty * Price) like SUMPRODUCT or an array formula"
XTL aggregates don't accept per-row arithmetic inside the argument.
{{ SUM([Qty] * [Price]) }}, {{ SUM([A] + [B]) }},
{{ AVERAGE([Net] - [Cost]) }} and similar shapes raise
xl3/eval/bad-aggregate-arg at parse time (ADR-0059). The argument
must be a single column reference: [Column] or Source[Column].
Three options, in order of preference:
- Helper column upstream — add an
Amountcolumn to the source (computed or pre-multiplied), then{{ SUM([Amount]) }}. This is the canonical XTL pattern for "sum of A × B" totals. - Native Excel
SUMPRODUCTin the footer cell — xl3 preserves cell formulas verbatim (ADR-0046). Write=SUMPRODUCT(E2:E10000, F2:F10000)directly in the footer cell. Use overshoot ranges (E2:E10000) since the rendered row count is unknown at authoring time. Mind the two footer pitfalls (self- column reference; row-overshoot double-count) — see LLM authoring guide § Footer pitfalls. - Per-row XTL cell + helper column in the rendered output —
put
{{ [Qty] * [Price] }}in a row-level cell (this works, it's a non-aggregate context) and footer it with{{ SUM([HelperColumn]) }}only if HelperColumn is also a source column. Otherwise you're back to option 1 or 2.
Why the restriction: XTL 0.x keeps the function surface small and
predictable. Per-row computed aggregation (Excel's array-formula
behavior) is a deferred feature — see ADR-0059 § "Why not allow
SUM([a] + [b])".
"I'm looking for SUMIF / COUNTIF / AVERAGEIF"
Don't reach for the function — use the data-block pattern. For "sum amounts where status = VIP":
{{ @filter [Status] = "VIP" }}
{{ @repeat down }}
... data row template ...
{{ SUM([Amount]) }}
If you need both the filtered total AND the unfiltered rows
displayed, put =SUMIF(B:B, "VIP", C:C) directly in the cell —
xl3 keeps the formula and Excel evaluates at open.
"I want ISBLANK(x)"
It exists as of 0.5.x (ADR-0047). Returns true when the value
is empty per ADR-0007 — including whitespace-only strings.
{{ IF(ISBLANK([Memo]), "(none)", [Memo]) }}
You can also use IFEMPTY([Memo], "(none)") for the fallback
form. They check the same predicate.
The general rule
Use XTL
{{ ... }}only when the value must be known before the workbook is written. Otherwise, put the formula in the cell and let Excel evaluate at open.
The boundary is render time:
- Before render — XTL only:
@filter,@sort,@top,@group,@subtotal, source-data aggregates (SUM,COUNT, …), cross-sourceXLOOKUP,output_file_pattern,__sheet_name_pattern__,__inputs__defaulting. Excel cannot reach into these — there's no cell for it to evaluate in. - After render — Excel is fine: cell display formatting, per-cell math on rendered values, string transformations on output values, type tests, date-component extraction from output cells.
The principle is normative — ADR-0043 — and it keeps the XTL function surface small by construction. Every Excel function not in XTL's table is intentionally an Excel-formula path.
Side-by-side cheat sheet
| Goal | XTL way | Excel-formula way | Pick |
|---|---|---|---|
Show 1,234,567.00 for a number | {{ TEXT([A], "#,##0.00") }} (string) | Cell numFmt = "#,##0.00", value {{ [A] }} (number) | Excel-formula for visual; XTL when you need the string |
Show ₩1,234,567 | (not supported in XTL) | Cell numFmt = "₩"#,##0 | Excel-formula |
| Show negative in parens | (not supported) | Cell numFmt = #,##0;(#,##0) | Excel-formula |
Per-row math (*2) | {{ [A] * 2 }} | =B2*2 ❌ doesn't rewrite per row | XTL |
| Footer SUM over expanding range | {{ SUM([A]) }} | =SUM(B:B) whole-column works | Either |
| Sum of A × B (SUMPRODUCT) | helper column upstream + {{ SUM([Amount]) }} | =SUMPRODUCT(E2:E10000, F2:F10000) in footer cell | Excel-formula or helper column — SUM([A]*[B]) raises xl3/eval/bad-aggregate-arg |
| Static hyperlink | (no need) | =HYPERLINK("...", "label") | Excel-formula |
| Per-row dynamic hyperlink | {{ HYPERLINK([Url], [Label]) }} | not feasible (quoting hell) | XTL |
| Filter rows for "this month" | {{ @filter MONTH([Date]) = MONTH(TODAY()) }} | (Excel can't filter pre-render) | XTL only |
| Filename "previous month" | {{ TEXT(EDATE(TODAY(), -1), "YYYY-MM") }}.xlsx | (no formula path in filename) | XTL only |
| Multi-branch tier label | {{ IFS([R]>10000, "VIP", [R]>1000, "Std", TRUE, "Lite") }} | =IFS(B2>10000, "VIP", ...) | Either; XTL when filter/group depends on it |
| Conditional aggregate | @filter + SUM block | =SUMIF(B:B, "VIP", C:C) | XTL for block totals; Excel-formula for cross-cutting |
MOD / INT / SQRT / POWER | (not supported in XTL) | Cell formula | Excel-formula |
| Blank check | ISBLANK([X]) or IFEMPTY([X], "fallback") | =ISBLANK(B2) | Either; ISBLANK matches the Excel idiom |
Other IS* type tests | (not supported) | =ISNUMBER(B2) etc. | Excel-formula |
Quick decision tree
Does the value affect:
• which rows render? → @filter / @sort (XTL)
• how rows are grouped? → @group / @subtotal (XTL)
• the output filename? → {{ ... }} (XTL)
• the sheet name? → {{ ... }} (XTL)
• a __inputs__ default? → {{ ... }} (XTL)
• per-row computed display? → {{ ... }} (XTL)
• how a cell *looks*? → cell numFmt (Excel-side)
• a per-row formula? → {{ ... }} expression (XTL)
• a whole-column / static calc?→ =FORMULA in cell (Excel-side)
Why this rule exists
The XTL function surface stays small by construction (ADR-0043) so porters have a clear catalog to implement. Adding functions for cell-output use only duplicates what Excel already does and bloats the spec.
The trade-off: an xl3 output workbook isn't fully self-contained when authors use cell formulas — opening it depends on Excel recalculating. For most operational reports this is the expected workflow.
When you find yourself reaching for a function XTL doesn't have:
- Is the value used inside a directive (
@filter,@sort,@top,@group,@subtotal) or in aoutput_file_pattern/__sheet_name_pattern__? → It must be XTL. If XTL doesn't provide what you need, file an issue using the "Function re-proposal" template (see GitHub issues). - Otherwise → put the Excel formula directly in the cell. xl3 preserves it; Excel evaluates at open time.
See also
- ADR-0043 — Excel-native preference principle
- ADR-0044 — Function batch accepted
- ADR-0045 — Function batch rejected
- ADR-0046 — Cell formula preservation contract
- ADR-0047 — ISBLANK as IFEMPTY alias
- Cookbook 10 — Styling and branding — when
numFmtis the right answer - Cookbook 11 — TEXT() formatting — when
TEXT()is the right answer - Cookbook 12 — Empty values in depth — IFEMPTY / ISBLANK companion