Skip to main content

Templates & Merge Variables

This page explains Keelstone's template object model — the three Salesforce objects that back every template — and how merge data flows from Salesforce records into your Office documents.


Template Object Model

Keelstone templates are managed through three related custom objects in the kstone namespace.


Keelstone_Template__c

The root object. Each record represents one document template and provides a stable identifier that you can reference in flows without hardcoding a file ID.

FieldTypeDescription
NameTextHuman-readable label (e.g. "Welcome Packet")
Template_Key__cText (80), UniqueDeveloper slug referenced in flows and embedded in generated documents. Stable across org migrations. Example: "welcome-packet"
Active__cCheckboxInactive templates are excluded from all lookups
Document_Type__cPicklistWord / Docs, Excel / Sheets, or PowerPoint / Slides — informational only; filtering is done via template key

The template file (.docx, .xlsx, or .pptx) is attached as a standard Salesforce File. KS: Generate Document always uses the most recently attached file.

Using a template in a flow:

KS: Build Merge Context   record → {!Get_Contact}    → contextJson
KS: Generate Document
templateKey → "welcome-packet"
contextJson → {!contextJson}
filename → "Welcome Packet.docx"
linkedEntityId → {!recordId}

Using templateKey means your flows are portable — they work in any org where a Keelstone_Template__c record with that key exists.


Keelstone_Template_Action__c

Junction object between Keelstone_Template__c and Keelstone_Action__c. Each record links one action to one template, controlling which action tiles appear in the taskpane when a generated document is open.

FieldTypeDescription
Template__cLookup (Keelstone_Template__c)The template
Action__cLookup (Keelstone_Action__c)The action tile to show for that template

Visibility rules:

  • An action with no Keelstone_Template_Action__c records is global — it appears in the taskpane for all open documents
  • An action with one or more junction records is template-scoped — it only appears when the open document was generated from a matching template

How matching works:

When KS: Generate Document generates a document using a templateKey, it embeds _ks_template_key in the document's custom properties. When the taskpane next opens, it reads that key and sends it to the server. The server then shows only actions that are either global or linked to that template.

Example:

ActionJunction RecordsShows when
Generate ItineraryLinked to welcome-packetDocument was generated from the welcome-packet template
Export to PDFNoneAll documents (global)

Merge Placeholder Syntax

Placeholders use single curly braces. The name inside matches the field path passed in the merge context.

Simple fields

{FirstName}           → Contact.FirstName
{Level__c} → Contact.Level__c
{Account.Name} → Contact.Account.Name (relationship field)
{Owner.Email} → Contact.Owner.Email

Field names are case-sensitive and must match the Salesforce API name exactly, including __c for custom fields. The field must be included in the Get Records SOQL query — fields not queried are unavailable at merge time.

Loop blocks (repeating rows or sections)

Use {#listName} / {/listName} to repeat content for each item in a collection. In Excel, rows between the tags are duplicated; in Word, the enclosed block (paragraph, table row, or multiple paragraphs) is repeated.

{#bookings}
{Experience_Name__c} | {Date__c} | {Number_of_Guests__c}
{/bookings}

The listName matches the tableName you set in the KS: Add Table flow action (or the key name in custom Apex / LWC context JSON).

Conditional blocks (Word only)

Show or hide content based on a truthy/falsy value:

{#isPremiumMember}
As a Premium member, you enjoy complimentary transfers.
{/isPremiumMember}

{^isPremiumMember}
Upgrade to Premium for exclusive member benefits.
{/isPremiumMember}

{#field} renders the block when the value is truthy (non-blank, non-zero, non-false). {^field} renders it when falsy.


Building merge data in a flow

Pass merge data to KS: Generate Document via contextJson. There are three ways to build it.

Path 1 — Pure admin (KS: Build Merge Context + KS: Add Table)

Get Records → Contact (Id, FirstName, LastName, Level__c, Account.Name)


KS: Build Merge Context record → {!Get_Contact} → contextJson
KS: Generate Document
templateKey → "welcome-packet"
contextJson → {!contextJson}
filename → "Welcome Letter.docx"
linkedEntityId → {!recordId}

Template:

Dear {FirstName} {LastName},

Welcome to {Level__c} member.
Your account: {Account.Name}
Get Records → Contact
Get Records → Booking__c (filter: Contact__c = recordId)
Get Records → Credit__c (filter: Contact__c = recordId)


KS: Build Merge Context record → {!Get_Contact} → contextJson
KS: Add Table records → {!Get_Bookings} → contextJson
tableName → "bookings"
KS: Add Table records → {!Get_Credits} → contextJson
tableName → "credits"
KS: Generate Document templateKey → "welcome-packet"
contextJson → {!contextJson}
filename → "Welcome Packet.docx"

Chain as many KS: Add Table calls as needed — there is no limit.

Path 2 — Custom Apex (cross-object fields, multiple lists in one step)

Write an @InvocableMethod that queries cross-object data (e.g. Owner.Name) and returns a contextJson String. Eliminates the need for separate Get Records elements and supports any data shape.

See the Developer Guide for a complete example.

Path 3 — LWC JavaScript

Build the context map in a headless Flow Screen LWC and pass the JSON string to KS: Generate Document via a flow variable.

Supplementing context with mergeData

mergeData is merged on top of contextJson at the highest priority. Use it from a Flow formula to inject computed values:

KS: Generate Document
templateKey → "pipeline-report"
contextJson → {!varContextJson}
mergeData → '{"generatedDate":"' & TEXT(TODAY()) & '"}'
filename → "Pipeline Report.xlsx"

Further reading