authoring guide

Template examples.

A Templ8r template is just a Word document with merge tokens. Drop {{column}} placeholders where you want values, save, upload — done. This page walks every binding shape with a side-by-side of the Word excerpt and the rendered output.

01Token basics

A token is the column's logical name wrapped in double curly braces. Templ8r reads the value off the record and pastes it back in the same place — preserving every Word run, font, colour, and table cell around it.

Word template

Order for {{name}}

Status: {{statecode}} · Created {{createdon}}

Total amount: {{totalamount}}

Rendered

Order for Acme Industrial Pumps

Status: Active · Created 06/05/2026

Total amount: £8,340.00

i
Use logical names, not display names. Templ8r reads from the Dataverse Web API, so {{name}} works but {{Order Name}} won't. Hover any column in the Templ8r mapper to see its logical name.

02Finding the logical names

Tokens use the column's logical name — the lowercase, no-spaces, publisher-prefixed identifier that Dataverse stores under the hood (e.g. name, totalamount, new_projectref). Display names like Order Name won't resolve. Below are the four fastest ways a sysadmin or power-user can pull a clean schema list.

Power Apps maker portal

The default route. Open make.powerapps.com → pick your environment → Tables → choose the table → Columns. Each row shows Display name, Name (logical name) and Data type. Click any column to see the full schema name, required level, and — for lookups — the related table. For relationships, switch to the Relationships tab and grab the Schema name (used for navigation properties on N:1 and 1:N tokens).

Solution explorer (classic)

For older orgs or audit work. Open the classic solution → expand your table → Fields and 1:N / N:1 / N:N Relationships. The Name column on each row is the schema name; the logical name is the same value lowercased. Useful when the modern UI hides system columns you actually need to bind to.

Dataverse Web API (bulk export)

The fastest way to grab the whole schema at once. Hit the metadata endpoint with your browser or a REST client:

# All columns for a table — logical + schema + display name
GET https://yourorg.crm.dynamics.com/api/data/v9.2/
  EntityDefinitions(LogicalName='account')/Attributes
  ?$select=LogicalName,SchemaName,AttributeType

# All N:1 lookups (gives you the navigation property name)
GET https://yourorg.crm.dynamics.com/api/data/v9.2/
  EntityDefinitions(LogicalName='account')/ManyToOneRelationships
  ?$select=ReferencingAttribute,ReferencedEntity,SchemaName,
  ReferencingEntityNavigationPropertyName
Paste in the browser tab where you're already signed into D365 — the response is JSON, copy-pasteable straight into a token list.

XrmToolBox / Level Up extension

Power-user shortcut. XrmToolBox (free) ships a Metadata Browser plugin that filters columns by table, type and prefix in one window — handy when you're standing up a template against an unfamiliar entity. The Level Up for Dynamics 365 Chrome extension adds an All fields button to any record form: it displays every column, its logical name, and the live value for that record — perfect for spot-checking a token before you paste it into Word.

i
Lookup gotcha. The lookup column (e.g. customerid) and its navigation property (e.g. customerid_account) can have different names. For tokens you want the navigation property — that's what comes after the dot. Polymorphic customer-type lookups expose one nav property per target table (customerid_account, customerid_contact); pick the one matching the related record.
?
Custom-prefix awareness. Custom columns and tables wear your publisher prefix — new_, tobyd_, contoso_, etc. The prefix is part of the logical name and case-sensitive in the URL but not in tokens. If a column doesn't resolve, double-check the prefix matches the publisher of the solution that owns the table.

03Lookups (N:1)

Reach into a related record with dot syntax. Reference the relationship's navigation property, then the column on the target entity. Lookup-type columns (Lookup, Owner, Customer) are auto-translated to their _col_value form — you write the friendly name, Templ8r handles the protocol.

Word template

Customer: {{customer.name}}

Primary contact: {{customer.primarycontactid.fullname}}

Account manager: {{ownerid.fullname}}

Rendered

Customer: Acme Industrial Pumps

Primary contact: Sarah O'Connell

Account manager: Jamie Reid

Token shape: {{relationshipName.column}} — where relationshipName is the schema name of the lookup column or its navigation property (e.g. customerid_account or primarycontactid).

04Multi-hop chains

Chain dots to walk N:1 lookups to any depth. Templ8r compiles the whole binding set into a single nested $expand with column-scoped $select at every hop — one record fetch, full document tree.

Word template

Bill to: {{customer.name}}

Parent group: {{customer.parentaccountid.name}}

Group region: {{customer.parentaccountid.territoryid.name}}

Region manager: {{customer.parentaccountid.territoryid.managerid.fullname}}

Rendered

Bill to: Acme Industrial Pumps

Parent group: Acme Holdings plc

Group region: EMEA North

Region manager: Lena Hartwell

Compiled query (peek under the hood):

// Templ8r builds this for you, given the four bindings above
GET /api/data/v9.2/salesorders({id})?
  $select=name
  &$expand=customerid_account(
    $select=name;
    $expand=parentaccountid(
      $select=name;
      $expand=territoryid(
        $select=name;
        $expand=managerid($select=fullname)
      )
    )
  )
i
Polymorphic lookups (e.g. the customerid column on a sales order, which can target either Account or Contact) work two ways:
  • Bare attribute (recommended). Bind to customerid and write {{customer.name}} — Templ8r fans out into one $expand fragment per target type in a single OData call and uses whichever variant the record actually points at. Same template works on records pointing at either Account or Contact.
  • Typed variant (legacy, still works). Bind explicitly to customerid_account or customerid_contact when you need target-specific fields. The visual mapper picks the right one if you tell it which target the column points at.

05Choice + status fields

Option-set, status, and statecode fields render as their friendly label by default — Dataverse provides a FormattedValue annotation alongside the raw integer, and Templ8r prefers the friendly form so your document reads naturally without any extra mapping.

Word template

Status: {{statecode}} ({{statuscode}})

Priority: {{prioritycode}}

Payment terms: {{paymenttermscode}}

Rendered

Status: Active (New)

Priority: High

Payment terms: Net 30

?
Templ8r prefers the friendly label by design. For choice / state / status / option-set columns, the engine reads the @OData.Community.Display.V1.FormattedValue annotation Dataverse returns alongside the raw integer. That’s usually what you want on a customer-facing document. If you specifically need the raw integer in the output (e.g. you're generating a CSV for a downstream system), bind a separate token to a calculated column that forces the integer form — or talk to us about a custom extension.

06Repeating rows (1:N)

For order lines, invoice items, contacts on an account — anything that's a 1:N collection. Place tokens inside a Word table row and Templ8r repeats the row for every related record. Header and footer rows pass through untouched.

Word template
ProductQtyUnitTotal
{{lines.productname}}{{lines.quantity}}{{lines.priceperunit}}{{lines.extendedamount}}
Rendered
ProductQtyUnitTotal
Centrifugal pump CF-2203£1,420.00£4,260.00
Pressure gauge PG-128£185.00£1,480.00
Pipe fitting kit (¾")2£1,300.00£2,600.00

Token shape: {{collectionName.column}} — where collectionName is the 1:N relationship name (e.g. order_details, aliased to lines in the mapper for readability).

i
Empty collections. If a record has no related rows, the entire repeating row drops out — header and footer remain. No empty placeholder rows, no “there are no items” text required.

07Custom entities

The token grammar doesn't care whether the entity is shipped by Microsoft or by your custom solution. Tokens against publisher-prefixed entities (cr1ad_project, acme_workorder, new_assessment) bind exactly the same way. The prefix just becomes part of the column name.

Word template

Project ref: {{cr1ad_projectref}}

Client: {{cr1ad_clientid_account.name}}

Health: {{cr1ad_health}}

Manager: {{cr1ad_projectmanagerid.fullname}}

Tasks completed: {{cr1ad_project_cr1ad_projecttask.cr1ad_taskname}} // 1:N to a custom child

Rendered

Project ref: PRJ-2026-0042

Client: Acme Industrial Pumps

Health: Amber

Manager: Lena Hartwell

Tasks completed: Site survey · Quote approved · Hardware delivered

Multi-hop, polymorphic, formatted-value, repeating rows, conditionals, aggregations — everything else on this page works identically against custom entities. The only difference is the prefix on the column / relationship / child-entity logical names.

?
Custom 1:N relationship names. Dataverse names them {parent}_{child} by default — e.g. cr1ad_project_cr1ad_projecttask. Field tokens nested inside use the full relationship name as the prefix: {{cr1ad_project_cr1ad_projecttask.cr1ad_completionpercent}}. Walk relationships in the visual mapper to confirm the exact schema names — they vary per solution.
i
Pre-built starters with a custom-prefix example. See Project Status Report and Service Work Order in the gallery — both are full templates against cr1ad_ custom entities. Swap the prefix for whatever your solution uses (acme_, new_, your_) and every other token shape stays the same.

08Conditional sections

Wrap a paragraph, table row, or whole document section in {{#if x}} ... {{/if}} guards. The merge engine drops the entire block when the column is empty, null, false, or zero — so empty risk notes, never-revised quotes, and accounts without follow-up actions just don’t print at all.

Word template

Project ref: {{cr1ad_projectref}}

Health: {{cr1ad_health}}

{{#if cr1ad_riskdescription}}

Risk note. {{cr1ad_riskdescription}}

{{/if}}

Rendered

Health: Green

Project ref: PRJ-2026-0042

Health: Green

(Risk note omitted — no risk on file.)


Health: Amber

Project ref: PRJ-2026-0058

Health: Amber

Risk note. Lead time on CF-220 pumps slipped two weeks; mitigation in flight with secondary supplier.

All three control-flow shapes:

{{#if x}} ... {{/if}}

Render the block when x is truthy (non-empty / non-null / non-zero / non-false). Choice columns are truthy whenever they have any value selected.

{{#unless x}} ... {{/unless}}

Inverse — render only when x is falsy. Useful for “outstanding” / “not yet completed” messaging.

{{#if x}} ... {{else}} ... {{/if}}

Two-branch fallback. Renders the second block when x is falsy. The two halves can carry completely different copy — e.g. paid vs. overdue messaging on an invoice.

i
Conditionals work on table rows too. Place the opening guard in a paragraph immediately above a row and the closing guard immediately below it. The whole row drops out when the guard is false — useful for “optional extras” rows that only print when configured.

Comparison operators (V2):

You can now compare a column against a literal directly inside the guard. Comparisons use keyword operators (not = or <) so Word’s autocorrect can’t mangle them. String comparisons are case-insensitive.

{{#if account.name eq "Cloud2020"}}

Render when the column equals the literal. Use ne for the inverse.

{{#if order.total gt 1000}}

Numeric comparisons: gt, lt, gte, lte. Both sides auto-coerce — numeric if both parse as numbers, then date in the document’s culture, then string.

{{#if contact.emailaddress1 endswith "@cloud2020.co.uk"}}

Substring matching: contains, startswith, endswith — all case-insensitive.

{{#if account.country in "UK","IE","FR"}}

List membership. Comma-separated string or numeric literals; quotes optional for numbers.

{{#if order.total gt 1000 and account.country eq "UK"}}

Combine guards with and / or. Strict left-to-right; no parentheses. Nest {{#if}} blocks for grouped logic.

Multi-branch with {{else if}}:

{{#if order.total gt 1000}}
  Big-order discount applies.
{{else if order.total gt 500}}
  Mid-tier discount applies.
{{else}}
  No discount.
{{/if}}
i
Quotes — either style. String literals accept both "double" and 'single' quotes. Word’s smart-quote autocorrect (curly “ ”) is normalised automatically — paste from anywhere.
i
Missing field ≡ empty. If a column is unbound or null, comparisons see it as an empty string. So {{#if x eq "Y"}} is false when x isn’t in the record. The bare {{#if x}} truthy form is preserved exactly — templates that worked before still work.
!
Markers belong on their own paragraphs. Each {{#if}}, {{else if}}, {{else}} and {{/if}} must occupy a paragraph of its own. Inline conditionals inside a sentence and conditionals split between a body paragraph and a table cell are not supported in V1 — the engine will throw a template error rather than silently miss the close.

09Aggregation functions

Roll up values across a 1:N collection without copy-pasting every line item into a Word formula field. Functions read the same merge context the repeating-row pass uses, so they cost zero extra Dataverse round-trips.

Word template

Lines on the order: {{count(lines)}}

Subtotal: {{sum(lines.extendedamount) | currency:GBP}}

Largest line: {{max(lines.extendedamount) | currency:GBP}}

Average unit price: {{avg(lines.priceperunit) | currency:GBP}}

Rendered

Lines on the order: 3

Subtotal: £8,340.00

Largest line: £4,260.00

Average unit price: £968.33

Five built-ins:

{{count(coll)}}

Number of rows in the collection. No field argument needed.

{{sum(coll.field)}}

Sum of field across every row. Non-numeric values skipped.

{{avg(coll.field)}}

Arithmetic mean. Empty collection yields 0.

{{min(coll.field)}}

Smallest numeric value across the collection.

{{max(coll.field)}}

Largest numeric value across the collection.

?
Aggregations run on the data Templ8r already fetched for the repeating-row pass — one OData call covers both the line-by-line table and the rolled-up summary at the foot.

010Number, date, currency

Append a format hint after a colon. Hints pass through to the same .NET formatting engine OpenXML merges use under the hood, so anything ICU/.NET supports works.

{{totalamount:C}}

Currency in tenant culture. £8,340.00 in en-GB, $8,340.00 in en-US. Use :C2 to force two decimals.

{{totalamount:N0}}

Number with thousands separator, no decimals. 8,340.

{{createdon:d}}

Short date in tenant culture. 06/05/2026 in en-GB, 5/6/2026 in en-US.

{{createdon:dd MMMM yyyy}}

Custom date format. 06 May 2026.

{{createdon:HH:mm}}

Time only, 24-hour. 14:32.

{{discount:P1}}

Percentage with one decimal. 12.5% for a stored value of 0.125.

i
Two shapes, one engine. Both are accepted — pick whichever reads better in your template.
  • Colon-direct (D365 / Word native): {{totalamount:C}}, {{createdon:dd MMM yyyy}}. The spec after : is passed straight to .ToString(spec, culture) — any standard .NET format string works, including ones with internal colons like HH:mm. Currency, percentage, short/long dates all follow tenant culture.
  • Pipe form (named formatters with options): {{totalamount | currency:GBP}}, {{createdon | date:short}}. Use this when you need to override the tenant culture — e.g. force GBP, USD, or EUR regardless of the running culture. Named formatters: currency, number, date, upper, lower.

011What gets fetched

Only the columns referenced in the template's bindings hit Dataverse. Templ8r builds an entity-scoped $select from your bindings, plus nested $expands for any relationship paths. No SELECT *, no over-fetching, no surprise rows in your audit log.

?
A template with eight scalar tokens and one relationship hop fires one Dataverse request that returns a JSON payload covering exactly those nine columns. A 1:N collection token wraps a side-array under the same response — still one round-trip.
Lookup → _col_value translation

Lookup, Owner and Customer columns are auto-rewritten to _col_value in the $select. You write {{ownerid.fullname}}, Templ8r requests _ownerid_value and walks the FormattedValue annotation to render Jamie Reid.

FormattedValue resolution

Choice fields, money columns and dates carry an @OData.Community.Display.V1.FormattedValue annotation. Templ8r prefers it over the raw integer/decimal so your document reads as a human would — without you mapping option-set keys to labels.

Token-level format hints

Format hints are applied after Dataverse responds, in our merge engine. They never alter the $select — so adding or removing :C2 doesn't change what gets fetched.

012Full sales-order example

Putting it all together. Below is the entire Word excerpt for a branded sales order — header, customer block, line items, totals — and what it renders for the demo Acme record.

Word template

SALES ORDER {{ordernumber}}

Created {{createdon:dd MMM yyyy}} · {{ownerid.fullname}}

Bill to

{{customer.name}}

{{customer.address1_composite}}

Account: {{customer.parentaccountid.name}}

ProductQtyUnitTotal
{{lines.productname}}{{lines.quantity}}{{lines.priceperunit:C}}{{lines.extendedamount:C}}

Subtotal: {{totalamount:C}}

Tax: {{totaltax:C}}

Due: {{totalamount_lineamount:C}}

Rendered

SALES ORDER SO-2026-0418

Created 06 May 2026 · Jamie Reid

Bill to

Acme Industrial Pumps

42 Foundry Lane, Sheffield S1 4AB, UK

Account: Acme Holdings plc

ProductQtyUnitTotal
Centrifugal pump CF-2203£1,420.00£4,260.00
Pressure gauge PG-128£185.00£1,480.00
Pipe fitting kit (¾")2£1,300.00£2,600.00

Subtotal: £8,340.00

Tax: £1,668.00

Due: £10,008.00

your turn

Bring your own template.

Drop tokens into your existing branded Word file and we'll wire it up against your Dataverse environment in a fifteen-minute call. No re-design, no redaction, no homework.