VerseDocs template syntax playground
Write a document template, paste the data it will run against, and see what comes out — before you install anything. Every token you reference is listed, every failure names the path that caused it, and both halves stay in your browser.
This is a browser reimplementation of the VerseDocs template language, for authoring and validation.
It implements the grammar in the syntax reference — the same tokens, the same truthiness rules, the same error codes. Real generation happens inside Dataverse, against real records, with your own columns and your own time zone.
Word placement rules cannot be reproduced in a text preview. Which table row repeats, which paragraph a conditional deletes, and how a token split across Word runs is stitched back together are all properties of the .docx, not of the token grammar. Here, a block tag alone on a line simply removes that line.
Four other things are stand-ins. Real generation stops at the first error; this keeps going, reports all of them at once, and writes an inline [[CODE: path]] marker where the value would have gone. Images are a Word feature, so the preview draws a labelled box and behaves as though you passed images named logo and signature — any other name is a real TEMPLATE_IMAGE_MISSING. Dates render the components the data actually holds, because a browser cannot know the calling user’s Dataverse time zone, and {{date}} and {{now}} read your own clock. And {{user.name}} is filled in so you can see it render, which the JSON path does not do — the cheat sheet below says why that one matters.
Start from a sample
Line items in a loop, and Subtotal, VAT and Total added up by the template itself. Note that the unformatted unit price keeps the digits the data had.
Options
The onMissing: blank switch. It is document-wide, not per token. It turns a missing path into an empty string, makes it falsy in a conditional and empty in a loop. A path that exists but holds null is not missing — it renders empty either way and never errors.
Template
What you would type into a Word document. Tokens only - no formatting.
Data
The JSON the tokens resolve against. From a record, VerseDocs builds this shape for you from the template's binding.
Output
21 tokens referenced, 0 errors, 0 unresolved.
INVOICE INV-2026-0148 Issued 13 August 2026, due 12 September 2026 Bill to Northwind Trading Ltd 12 Harbour Road Bristol BS1 4TR 1 x Template Studio setup at 1,800.00 = 1,800.00 3 x Dataverse binding review at 240.00 = 720.00 1 x Handover workshop at 450.00 = 450.00 Lines 3 Subtotal 2,970.00 VAT 594.00 Total GBP 3,564.00 Largest line 1,800.00, average 990.00. Unit price straight from the data, no formatter: 1800.00 Generated 2026-08-13 09:30 by Alex Admin
Errors (0)
Every token resolved. This template would render against this data.
Preview limitations hit by this template
user.name and user.email are filled in here so the token renders. VerseDocs resolves the calling user on the row-bound path only - when you pass your own JSON they are not resolved, and referencing them is TEMPLATE_FIELD_MISSING like any other path that is not in the data.
Tokens referenced (21)
Every path this template needs, including the ones inside blocks that did not run. This is the list to check a binding against.
invoice.numberresolvedinvoice.date:dd MMMM yyyyresolvedinvoice.dueDate:dd MMMM yyyyresolvedcustomer.nameresolvedcustomer.address.line1resolvedcustomer.address.cityresolvedcustomer.address.postcoderesolved#each linesresolvedqtyresolved x3itemresolved x3price:N2resolved x3amount:N2resolved x3count:linesresolvedsum:lines.amount:N2resolvedsum:lines.vat:N2resolvedsum:lines.total:N2resolvedmax:lines.amount:N2resolvedavg:lines.amount:N2resolvedlines[0].priceresolvednow:yyyy-MM-dd HH:mmresolveduser.nameresolvedSyntax cheat sheet
Full referenceFields
{{customer.name}}Dots walk into nested objects. Field names are identifier-shaped: a letter or underscore, then letters, digits or underscores.
{{lines[0].item}}Array indexes are zero-based and can repeat, as in {{a[0][1]}}.
{{account._raw.revenue}}From a record, every column is reachable twice: the formatted value Dataverse shows a user, and the underlying value under _raw.
{{customer.nmae}}A path that does not exist is TEMPLATE_FIELD_MISSING, naming the path. A typo fails loudly instead of silently producing a document with a blank where a customer’s name should be.
Formatters
{{total:N2}}Thousands separators and two decimals. N0, N4, F0, P, D5 and custom patterns like #,##0.00 all work.
{{invoiceDate:yyyy-MM-dd}}Date patterns: yyyy MM dd HH mm ss, plus MMM, MMMM, ddd, dddd.
{{startTime:HH:mm:ss}}Only the first colon splits the path from the format, so time formats work exactly as written.
{{isActive:N2}}The value’s type decides the rule, not the format string. A number is a decimal; a string is tried as a date then as a number; a formatter on a boolean, array or object is TEMPLATE_INVALID_FORMAT.
{{price}}With no formatter, a value renders as raw text. A number keeps exactly the digits the data had, so 30.00 stays 30.00. Booleans render true / false, and null renders empty.
Total: GBP {{total:N2}}Format money with :N2 and a literal symbol. The :C formatter renders the generic currency sign, because the render culture is invariant.
Loops
{{#each lines}}{{item}}{{/each}}The context becomes each element in turn, so bare tokens resolve against that element. In Word, both tags in one paragraph repeat that whole paragraph, including any text either side of them — the one placement rule worth knowing before you author, because this preview repeats only what sits between the tags. Give loop tags their own paragraph or their own table row.
{{#each lines}}{{customer.name}}{{/each}}Fails. There is no outer scope inside a loop — including the built-in tokens, which are merged at the data root. Everything a row needs has to be on the row object.
{{#each customer}}A target that is not an array is TEMPLATE_EACH_TARGET_NOT_ARRAY. In Word, where you put the tags decides what repeats: both tags in one table row repeats that row.
Conditionals
{{#if poNumber}}PO: {{poNumber}}{{/if}}Kept when the value is truthy: a true boolean, a non-zero number, a non-empty string, a non-empty array, or any object. Null never is.
{{#unless poNumber}}None on file.{{/unless}}The inverse. Put the label inside the block, or a false condition leaves the label behind with nothing after it.
{{#if customer.vip}}A missing path in a condition is TEMPLATE_FIELD_MISSING, not false. A conditional that quietly swallowed typos would be worse than useless. Only onMissing: blank makes it falsy.
Totals and counts
{{sum:lines.price:N2}}The last segment is the field, everything before it is the collection. Formatters apply to the result.
{{count:lines}}How many items. {{lines.length}} is not supported. Also avg:, min: and max:.
{{sum:items.value}}Inside a loop, the path resolves against the current item, so this totals each group separately.
"$1,800.00"Pre-formatted currency still adds up: symbols and separators are stripped and accounting parentheses read as negative. Blank, null and missing count as zero. A value that genuinely is not a number fails with TEMPLATE_INVALID_FORMAT naming it, because a total that quietly leaves out a line is worse than no total.
Built-in tokens
{{date}} {{today}} {{now}}Merged in automatically, so a template can say when it was generated without anyone supplying it. Real data always wins: a top-level field of your own with one of these names is used instead. Here they come from your browser clock; in Dataverse they come from the calling user’s own time zone.
{{user.name}} {{user.email}}The calling user, resolved only on the row-bound path. Generate from JSON you supply — a flow, or this page — and they are not resolved at all, so the token is TEMPLATE_FIELD_MISSING like any other absent path. The playground fills them in so you can see the token render; a flow will not. If a document has to be signed off by a named person, put that person on the record.
Images and errors
{{image:logo width=40mm}}A Word feature. The name is looked up in the images map you pass in the call’s options; PNG and JPEG are detected from the bytes, not from a file name. Give a width, a height, or both — the missing dimension follows the image’s own aspect ratio, and giving both stretches it to fit. No size at all is TEMPLATE_SYNTAX_ERROR, and a name that is not in the map is TEMPLATE_IMAGE_MISSING even under onMissing: blank, because there is no sensible blank image.
TEMPLATE_FIELD_MISSINGThe five template error codes are this, TEMPLATE_INVALID_FORMAT, TEMPLATE_EACH_TARGET_NOT_ARRAY, TEMPLATE_IMAGE_MISSING and TEMPLATE_SYNTAX_ERROR. Excel workbooks add two more for the rows named range.
Common questions
- Does anything I paste here leave my browser?
- No. The engine is JavaScript in this page, and the site is a static export — there are no API routes and no server that could receive a template. Being exact about it: the page loads Google Analytics, which records the page view and whether you clicked through to the documentation — and nothing from the two text boxes, so a real template and real record data stay in the tab. If you would rather not take that on trust, load the page, disconnect, and it still renders.
- Does this validate my actual Word document?
- No, and the difference matters. It validates the token language: paths, formatters, block structure, and whether each token resolves against the data you give it. It cannot see your .docx, so it cannot tell you which table row will repeat or which paragraph a conditional will delete — those come from where the tags physically sit in the document. Use the Validate tab in Template Studio for the real file, which also checks every path against the bound table and suggests corrections.
- Why is a missing field an error rather than a blank?
- Because a blank is how a bad document ships. A misspelled path that silently renders empty produces an invoice with no customer name and no warning that anything went wrong. Failing names the path so you fix it in seconds. Set onMissing to blank when a template genuinely runs against records with optional columns — it is a document-wide switch, and it also makes those paths falsy in conditionals. A column that exists but is empty on this row is a different thing entirely: it renders empty and never errors, whatever onMissing says.
- Why does {{customer.name}} fail inside {{#each lines}}?
- Inside a loop the context is replaced by the current element, and there is no fallback to the outer scope. A line item object has no customer property, so the path does not exist. Everything a row needs must be on the row object itself — which in practice means putting it in the related-table projection, or moving that token outside the loop. The same rule catches the built-in tokens: they are merged at the data root, so a date token inside a loop is missing too.
- Can a template total its own line items?
- Yes. {{sum:lineitems.price:N2}}, {{count:lineitems}}, {{avg:...}}, {{min:...}} and {{max:...}} run over the collection at render time, so an invoice does not need its subtotal calculated by a flow and stored on the record first. Values that arrived as formatted currency strings still add up, and one value that is not a number fails the whole total rather than being skipped.
This is the language. VerseDocs is the engine.
Templates are ordinary Word, Excel and PowerPoint files, and generation runs inside Dataverse - no external service, no document data leaving your tenant. Template Studio validates a real file against a real table before you ship it.
Keep going
Template syntax reference
The complete language: fields, formatters, loops, conditionals, images and the Dataverse data shape, including the Word and Excel placement rules a text preview cannot show.
Document automation ROI
What the documents your team assembles by hand actually cost, in hours and in money.
VerseDocs documentation
Install, bind a template to a table, generate from a record, and call it from a flow.
Free resources
Governance policies, checklists and templates in Word, ready to adapt. One address unlocks every file.