Template Syntax

Templates are ordinary Word, Excel and PowerPoint files. You type tokens where values should go. This page is the complete language reference — fields, formatters, loops, conditionals, images and the Dataverse data shape.

Fields and formatters (sections 1 and 2) work identically in Word, Excel and PowerPoint. Loops and conditionals work in Word and PowerPoint, under the same rules; Excel has its own row-repeat mechanism instead. Images are a Word feature. Each is covered below.
Try it without installing anything. The template playground runs this syntax in your browser against sample data, so you can check a template renders and every token resolves before you go near a real environment.

1. Fields

{{customer.name}}
{{customer.address.city}}
{{lines[0].item}}
  • Dots walk into nested objects: {{customer.address.city}}.
  • Array indexes are zero-based and can repeat: {{lines[0].item}}, {{a[0][1]}}.
  • Field names must look like identifiers — a letter or underscore, then letters, digits or underscores.

Missing fields

By default a field path that does not exist is an error: TEMPLATE_FIELD_MISSING, naming the exact path as you wrote it. This is deliberate — it means a typo fails loudly instead of silently producing a document with a blank where a customer’s name should be.

Set "onMissing": "blank" in the call’s options to render missing fields as an empty string instead. It is a document-wide switch, not per token, and it also makes missing paths falsy in conditionals and empty in loops.

A field that exists but is empty is not missing. A JSON value of null, and a Dataverse column with no value on that row, both render as an empty string and never trigger TEMPLATE_FIELD_MISSING — regardless of onMissing. Only a path that does not exist at all is missing.
A template in the editor showing field tokens, a date formatter and an each loop inside a table row.
Every construct on this page, in one template: fields, a date formatter, and a loop over a related table.

2. Formatters

{{total:N2}}                 1234.5    ->  1,234.50
{{invoiceDate:yyyy-MM-dd}}   a date    ->  2026-08-13
{{startTime:HH:mm:ss}}       a time    ->  09:30:00

Append :format to a field to format it. Formats are standard .NET format strings, applied using the culture you pass in Options.culture (for example "en-GB"); leave it unset for invariant culture.

Which rule applies is decided by the value’s own type, not by the format string:

Value is aTreated asNotes
JSON numberDecimalN2, F2, C2, 0.00 and friends.
JSON stringDate first, then numberTried as a date/time under the active culture; if that fails, tried as a decimal — which covers numbers that arrived quoted.
Anything elseErrorA formatter on a boolean, array or object is TEMPLATE_INVALID_FORMAT. Formatters only make sense on scalars.

Without a formatter, a value renders as its raw text: numbers keep exactly the digits the data had (so 30.00 stays 30.00, not 30), booleans render true/false, and null renders empty.

Only the first colon splits the path from the format, so time formats like {{startTime:HH:mm:ss}} work exactly as written.

3. Loops — Word

{{#each lines}}
  {{item}}: {{qty}} x {{price:N2}}
{{/each}}

Inside the block, the current context becomes each array element in turn, so bare tokens like {{item}} resolve against that element.

There is no fallback to the outer scope inside a loop. {{#each lines}}{{customer.name}}{{/each}} will not find the customer — everything a row needs must be on the row object itself.

The target must be an array. Anything else is TEMPLATE_EACH_TARGET_NOT_ARRAY; a missing path is TEMPLATE_FIELD_MISSING unless onMissing is blank, in which case the block simply renders nothing.

Repeating a table row

The engine works out what to repeat from where you physically put the tags. You do not configure this:

Where the tags areWhat repeats
Both in cells of the same table rowThat row, once per item. This is the classic invoice line-item pattern — {{#each lines}} in the first cell, {{/each}} in the last cell of the same row, fields in between.
In different rows of the same tableEvery row from the opening tag's row to the closing tag's row inclusive — for a line item that needs more than one visual row.
Anywhere elseThe paragraphs from the opening tag's paragraph through the closing tag's paragraph, inclusive.
If both tags sit in the same paragraph, that whole paragraph repeats — including any text before {{#each}} or after {{/each}} in it. Put loop tags in their own paragraph or their own table row unless you want the surrounding text repeated too.

4. Conditionals — Word

{{#if poNumber}}PO: {{poNumber}}{{/if}}
{{#unless poNumber}}No purchase order on file.{{/unless}}

{{#if}} keeps its content when the value is truthy; {{#unless}} keeps its content when it is falsy.

ValueTruthy when
BooleanIt is true.
NumberIt is non-zero.
StringIt is non-empty.
ArrayIt has at least one element.
ObjectAlways — presence is the signal.
null, or missing under blankNever.
A missing path in a condition is TEMPLATE_FIELD_MISSING by default, exactly like a plain field. {{#if}} does not quietly treat “not in the data at all” as false — that would make it a silent typo-swallower. Only onMissing: blank makes it falsy.

Which region gets kept or removed follows the same row / row-group / paragraph rules as loops above.

Conditionals remove only what they guard. In PO: {{#if po}}{{po}}{{/if}} (ref) a false po drops the value and leaves PO: and (ref) in place, because they sit outside the conditional. To drop a whole line, put its label inside: {{#if po}}PO: {{po}}{{/if}} — a conditional covering its entire paragraph removes the paragraph rather than leaving a blank line.

4b. Totals and counts

A template can add up its own line items, so an invoice does not need its Subtotal, VAT and Total calculated somewhere else and stored on the record first.

{{#each lineitems}}{{name}}  {{qty}}  {{price}}
{{/each}}
Lines:    {{count:lineitems}}
Subtotal: {{sum:lineitems.price:N2}}
Cheapest: {{min:lineitems.price}}
HelperReturns
{{sum:rows.price}}The total of that field across the collection.
{{count:rows}}How many items — no field, because counting needs none.
{{avg:rows.price}}The mean.
{{min:rows.price}}The smallest value.
{{max:rows.price}}The largest value.

The last segment is the field, everything before it is the collection. So {{sum:account.lineitems.amount}} totals amount across account.lineitems. Formatters apply to the result: {{sum:lineitems.price:N2}}. Inside a loop, the path resolves against the current item, so {{sum:items.value}} within {{#each groups}} totals each group separately.

Pre-formatted values still add up. When you generate from a Dataverse row, a currency column arrives as $1,800.00 rather than 1800 (section 7). Aggregates cope with that — the symbol and separators are stripped and the number recovered, with accounting parentheses read as negative. Blank, null and missing values count as zero. A value that genuinely is not a number fails with TEMPLATE_INVALID_FORMAT naming it, rather than being skipped: a total that quietly leaves out a line is worse than no total.

{{rows.length}} is not supported — use {{count:rows}}.

Format money with :N2 and a literal symbol — Total: £{{sum:lineitems.price:N2}}. The :C formatter currently renders the generic currency sign ¤ rather than your own, because the render culture is invariant.

5. Images — Word

{{image:logo width=40mm}}
{{image:signature height=15mm}}
{{image:stamp width=25mm height=25mm}}

The name after image: is looked up in the images map you pass in the call’s options — a name to image-bytes map. PNG and JPEG are supported, and the format is detected from the bytes rather than from any file name.

You giveYou get
width onlyHeight computed from the image's own aspect ratio.
height onlyWidth computed from the image's own aspect ratio.
bothBoth used exactly — aspect ratio is not preserved, because you asked for an exact box.
neitherTEMPLATE_SYNTAX_ERROR — a size is required.
A name that is not in the images map is always TEMPLATE_IMAGE_MISSING, even under onMissing: blank — there is no sensible “blank image”.
Barcodes go in this way. Generate one with vdocs_BarcodeGenerate, pass the returned image as a named image, and place {{image:barcode width=40mm}} in the template. Give a QR code only a width so it stays square.

6. Built-in tokens

A few ambient values are merged in automatically, so a template can say “when was this generated” without anyone supplying it:

TokenMeaningFormattable
dateGeneration date, no time, in the calling user's own time zone.Yes
todayIdentical to date, under a friendlier name.Yes
nowGeneration date and time, in the calling user's own time zone.Yes
user.nameThe calling user's display name, when resolvable.No
user.emailThe calling user's email, when resolvable.No
{{date:yyyy-MM-dd}}   ->  2026-08-13
{{now:HH:mm}}         ->  14:23
{{user.name}}         ->  Alex Admin
  • Real data always wins. If your data already has a top-level field with one of these names, yours is used.
  • Time zone. Dates are converted into the calling user’s own Dataverse time zone, using Dataverse’s own conversion, so daylight saving is handled correctly. In a flow, the “calling user” is the account the flow runs as — set that account’s time zone deliberately.
  • Never a failure. If the time zone cannot be resolved for any reason, the value falls back to UTC and the document still renders. A time zone lookup will never be the reason a generation fails.
  • user.name and user.email are best-effort. They are resolved on the row-bound path. When generating from JSON you supply, they are not resolved, and referencing them behaves like any other missing field.
There is deliberately no built-in for a future date such as a quote’s “valid until”. Format specifiers do no date arithmetic, and an expiry equal to the generation date is not a placeholder — it is a wrong answer that renders successfully and ships a quote that expired the day it was issued. Supply that value yourself.

7. Generating from a Dataverse row

When a document is generated from a record — the Generate document button, or the row-bound actions — the data is built for you from the template’s binding. Everything above still applies; this section describes the shape your tokens walk.

Formatted values and raw values

The row sits under its table’s logical name, and every column is reachable two ways:

{{account.statuscode}}        ->  Active      (formatted — what a user would see)
{{account._raw.statuscode}}   ->  1           (raw — the underlying value)
  • Formatted (the default) is Dataverse’s own human-readable string: Contoso Ltd rather than a GUID, Active rather than 1, $1,234.00 rather than 1234. Where Dataverse has no formatted value — plain text and whole-number columns usually do not — it falls back to the raw value.
  • _raw is the underlying value with its native type: a number for currency and option sets, a GUID string for a lookup, an array of codes for a multi-select. Use it when you want to apply your own formatter or branch on a code.

What each column type renders as

Column typeFormatted (default)_raw
TextThe textSame
Option setThe label, e.g. AccountingThe code, e.g. 1
Multi-select option setAll selected labels, semicolon-separated, e.g. Newsletter; EventsAn array of codes, e.g. [676310001,676310003]
CurrencyWith a symbol, e.g. $125,000.50Plain decimal, e.g. 125000.5000
Whole numberDataverse supplies no formatted value, so this falls back to the raw numberSame integer
Two options (yes/no)That column's own configured labels — not a generic Yes/Notrue / false
Date and timeThe calling user's own locale and time zoneISO 8601 UTC
LookupThe related record's display name, never a GUIDThe related record's id
Options.culture does not affect formatted currency. That string comes from Dataverse itself, computed from the organisation’s base currency and the calling user’s locale. If you need a specific culture regardless, format the raw value yourself: {{account._raw.revenue:N2}}.
A boolean column’s formatted text is whatever that column’s True/False labels are set to — on the standard “Do not allow email” column, for instance, they read Do Not Allow and Allow. Check the column rather than assuming Yes/No.

Every related table in the binding becomes an array under its alias, whatever the relationship type. Loop over it like any other array:

{{#each account.contacts}}
  {{fullname}} — {{jobtitle}}
  {{#each activities}}
    Task: {{subject}}
  {{/each}}
{{/each}}

This is uniform on purpose. A one-to-many relationship naturally holds many rows, and a lookup is also projected as an array — of zero or one element — rather than a bare object, so you use one pattern everywhere instead of learning a second shape per cardinality:

{{#each account.primarycontact}}Primary contact: {{fullname}}{{/each}}
{{account.primarycontact[0].fullname}}     equivalent, if you know there is exactly one

Nesting recurses to any depth, and many-to-many relationships need no special syntax.

Empty columns render empty

A real column with no value on this row renders as an empty string — including lookups, option sets, dates and booleans, and including when a formatter is attached, so {{contact.birthdate:yyyy-MM-dd}} on a contact with no birthdate renders empty rather than erroring or printing a nonsense default date.

A column that does not exist on the table at all still errors with TEMPLATE_FIELD_MISSING. That distinction is the point: an empty field is normal, a misspelled field is a bug, and they are not treated the same way.

8. Why tokens survive Word's editing

Word routinely stores what you typed as one token across several internal fragments — {{cus / tomer. / name}} — after a spellcheck pass, a copy-paste, or simply a second editing session. It is invisible in the UI and very real in the file.

VerseDocs handles this transparently: it reads each paragraph as one continuous string, finds tokens there, and writes the value into the first fragment the token touched, keeping that fragment’s formatting. Text and formatting outside the token are never disturbed. PowerPoint gets the identical treatment.

Practically: you never have to retype a token that “looks fine but does not work”, and a bold word next to a token keeps its bolding. A token renders using the formatting of wherever its opening {{ began — so format the whole token consistently if you care how the result looks.

9. Headers and footers

Tokens in headers and footers render exactly like tokens in the body — same engine, same rules. Footnotes and endnotes are not rendered.

10. Excel templates

Put {{path}} in any cell on any worksheet. Fields and formatters behave exactly as in sections 1 and 2.

  • Formulas are never touched. A cell starting = is left exactly as authored.
  • A cell containing only one plain token keeps its type. A cell whose entire content is {{price}} — no surrounding text, no formatter — becomes a real Excel number, so formulas referencing it still compute. Any other shape produces a text cell.

Repeating rows: the rows named range

Excel’s equivalent of a Word line-item loop. Define a named range called exactly rows spanning one row (any number of columns), typically the line-item row under your headers. That row is duplicated once per element of the top-level rows array in your data.

  • Bare tokens in the row resolve against that array element, exactly like a Word loop.
  • Formulas shift correctly. =B4*C4 on the template row becomes =B5*C5 on the next, just as if you had copied the row in Excel.
  • Styles and row height are preserved on every duplicated row.
  • Zero items deletes the template row; one renders in place; N inserts N−1 rows below and shifts everything under them down — plan a totals row with that in mind.
  • The rows name is removed from the output workbook, so recipients never see the authoring marker.
SituationResult
Your rows data is not an arrayTEMPLATE_EACH_TARGET_NOT_ARRAY
Valid array, but the workbook has no rows named rangeXLSX_ROWS_RANGE_MISSING
The rows name spans more than one row, or is defined more than onceXLSX_INVALID_ROWS_RANGE
Neither a rows range nor rows dataNot an error — a plain field-only workbook never touches this feature.

11. PowerPoint templates

Fields, formatters, loops and conditionals all work in decks, using the same grammar and the same scoping rules as Word — plus one rule decks have and documents cannot.

Tokens work anywhere text goes: titles, body placeholders, any text box, and table cells, which need no special syntax. Slide content is rendered; slide masters and layouts are not.

Blocks inside one slide

{{#each}}, {{#if}} and {{#unless}} follow section 3’s rules exactly: tags in the same table row repeat that row, tags in different rows of one table repeat the row range, and anything else repeats the sibling range between the tags — paragraphs within one text box, or whole shapes when the tags sit in different shapes.

When a loop clones shapes, each copy is renumbered automatically. A slide’s shape ids must be unique, and duplicates are what make PowerPoint announce a file that “needs repair”.

Repeating whole slides

Put {{#each products}} on one slide and {{/each}} on a later one, and every slide from the first through the last repeats, once per item — one slide per product, per region, per site. Each copy renders against its own item, so within-slide loops on those slides resolve per item too.

A three-slide deck: intro, body, closing
Slide 1:  Catalogue {{#each products}}
Slide 2:  Product: {{name}}      <- repeats per product
Slide 3:  End {{/each}}

{{#if}} and {{#unless}} spanning slides keep or drop the same range — how an appendix section gets included only when it applies.

  • Zero items removes every slide in the range.
  • Tags are stripped before any slide is copied, so no copy carries a leftover tag.
  • Copies share the layout, images and hyperlinks they point at rather than duplicating them, and the deck's slide list is rewritten with fresh unique ids.
  • A slide-spanning block nested inside another slide-spanning block is UNSUPPORTED_FEATURE. Keep the inner block within one slide.
Tags on the same slide are not this feature — they resolve inside that slide by the ordinary rules above.

Images are not supported in decks

{{image:name}} raises UNSUPPORTED_FEATURE in a PowerPoint template. This one is structural rather than a choice: a PowerPoint picture is a shape in the slide’s shape tree, not something that lives inside a line of text the way a Word inline image does, so a token sitting in a paragraph carries no position to place a picture at.

12. Template error codes

CodeCause
TEMPLATE_FIELD_MISSINGA token references a path that does not exist. The message names the exact path.
TEMPLATE_INVALID_FORMATA formatter was applied to a value it cannot format — a boolean, array or object, or a string that is neither a date nor a number.
TEMPLATE_EACH_TARGET_NOT_ARRAYA {{#each}} target resolved to something that is not an array.
TEMPLATE_IMAGE_MISSINGAn image token names an image that was not supplied.
TEMPLATE_SYNTAX_ERRORA malformed token — most often an image token with no width or height.
XLSX_ROWS_RANGE_MISSINGRow data was supplied but the workbook defines no rows range.
XLSX_INVALID_ROWS_RANGEThe rows range spans more than one row, or is defined more than once.
Run the Validate tab in Template Studio before shipping a template. It catches every TEMPLATE_FIELD_MISSING that a typo would cause, at authoring time, with a suggested correction.
Validation results confirming all field paths in a template resolve against the bound table.
When you are unsure a path is right, Validate answers it in a second.