Template Creation Guide
This guide is for Salesforce administrators who want to create document templates that Keelstone can merge with Salesforce data — no developer required.
Overview
A Keelstone template is a standard Office file (Word .docx or Excel .xlsx) that contains merge placeholders — tokens that Keelstone replaces with live Salesforce data when a user runs the action.
The workflow is:
- Create a template file in Office with merge placeholders
- Create a Keelstone Template record in Salesforce and attach the file to it
- Build a Flow that uses KS: Generate Document to merge and save the output
- Create a Keelstone Action record pointing to that flow
No custom code or LWC is required. Flows pass Salesforce records directly to the action using standard Get Records elements.
The Keelstone Template Object
Keelstone_Template__c is a Salesforce custom object that acts as a stable registry for your template files. Instead of hardcoding a ContentDocumentId in your flows (which changes if you move orgs or reinstall), you reference the template by a developer-chosen Template Key slug that you control.
Fields
| Field | Type | Description |
|---|---|---|
Template Name | Text | Human-readable label for the template |
Template Key | Text (80), Unique | Developer slug used in flows and embedded in documents. Example: "welcome-packet". Stable across org migrations. |
Active | Checkbox | Inactive templates are ignored |
Document Type | Picklist | Word, Excel, or PowerPoint — Excel and PowerPoint require the Pro plan |
The template file is attached using standard Salesforce Files (ContentDocumentLink). KS: Generate Document looks up the most recently attached file when generating.
Creating a template record
- Go to Keelstone Templates in Salesforce (custom object tab or App Launcher)
- Create a new record:
- Template Name:
Welcome Packet - Template Key:
welcome-packet - Document Type:
Word - Active: ✓
- Template Name:
- Open the record and click Files → Upload Files to attach your
.docxtemplate
Using the template in a flow
Reference the template by key — no ContentDocumentId needed:
KS: Build Merge Context record → {!Get_Contact} → contextJson
KS: Generate Document
templateKey → "welcome-packet"
contextJson → {!contextJson}
filename → "Welcome Packet.docx"
linkedEntityId → {!recordId}
The action looks up the latest file attached to that template record automatically. Your flow works in any org where a template record with that key exists.
Upload a new version of the file to the template record. The Template Key stays the same — flows need no changes. Keelstone always uses the most recently uploaded file.
Template-Scoped Actions
By default, every active Keelstone Action record appears in the taskpane for all open documents. Template-scoped actions let you show or hide specific action tiles based on which template was used to generate the current document.
How it works
When Keelstone generates a document using a template key:
- It embeds
_ks_template_key(the slug) in the Word or Excel document's custom properties - On next taskpane open, the taskpane reads this property and sends it to the server
- The server filters actions: global actions (no junction records) always show; template-scoped actions only show when the template key matches
Configuring template-scoped actions
The Keelstone_Template_Action__c junction object links a template to an action:
- Open a
Keelstone_Action__crecord - In the Template Actions related list, click New
- Select the
Keelstone_Template__crecord this action should appear for
Once a Keelstone_Template_Action__c junction record exists on an action, that action becomes template-scoped and only appears on documents generated from a matching template. An action with no junction records remains global and appears on all documents.
Example
| Action | Junction Records | Appears when |
|---|---|---|
CC: Scan Guest Interests | Linked to welcome-packet template | Document was generated from the welcome-packet template |
KS: Export to PDF | None | All documents (global) |
CC: Generate Itinerary | Linked to itinerary template | Document was generated from the itinerary template |
Merge Placeholder Syntax
Placeholders use single curly braces and match the Salesforce API field name exactly:
{FieldApiName}
| Template text | Field value | Output |
|---|---|---|
Dear {FirstName}, | FirstName = "Jane" | Dear Jane, |
Level: {Level__c} | Level__c = "Gold" | Level: Gold |
Total: {Amount} | Amount = 50000 | Total: 50000 |
Placeholder names are case-sensitive and must exactly match the API name as it appears in Salesforce — including the __c suffix for custom fields.
Relationship fields
If the queried record includes related object fields (e.g. Account.Name), access them with dot notation:
{Account.Name}
{Owner.Email}
{Session__r.Location__c}
Use the exact SOQL relationship path. If your flow queries SELECT Name, Account.Name FROM Contact, use {Account.Name} in the template.
Excel Templates
Basic data merge
Place placeholders directly in cells:
| Cell | Template value | Result |
|---|---|---|
| B2 | {Name} | Acme Corporation |
| B3 | {Amount} | 50000 |
| B4 | {CloseDate} | 2026-03-31 |
You can embed a placeholder inside static text:
Prepared for: {Account.Name} — {Name}
Row repeating (table data)
To repeat rows for a collection of records, use a loop block. The table name is the tableName you set in the KS: Add Table flow action:
{#bookings}
{Experience_Name__c} | {Date__c} | {Number_of_Guests__c}
{/bookings}
Rows between {#bookings} and {/bookings} are duplicated for each record in the collection. Inside the loop, use the field's API name directly — no prefix needed.
Formatting tips
- All formatting (fonts, colors, borders, formulas, charts) is preserved exactly as designed
- Cells with placeholders can have any format — number formatting on the cell is respected if you pass a numeric value
- Merged cells work — place the placeholder in the top-left cell of the merged region
- Conditional formatting and named ranges are preserved
Word Templates
Inline placeholders
Place {FieldName} anywhere in the document body, headers, footers, or text boxes:
This Agreement is entered into as of {EffectiveDate__c} between
{Account.Name} ("Company") and {Name} ("Customer").
Repeating sections
Use {#listName} / {/listName} around any block of content — a paragraph, a table row, or multiple paragraphs — to repeat it for each record in a collection:
{#lineItems}
• {Name} — {Quantity__c} × {Unit_Price__c}
{/lineItems}
Inside the loop, reference fields by their API name directly.
Repeating table rows
Place the loop tags inside a table row to repeat just that row:
| Experience | Date | Guests |
|---|---|---|
{#bookings}{Experience_Name__c} | {Date__c} | {Number_of_Guests__c}{/bookings} |
The {#bookings} tag can go at the start of the first cell and {/bookings} at the end of the last cell in the same row.
Conditional sections
Show or hide a block based on a truthy value:
{#isPremiumMember}
As a Premium member, you enjoy complimentary transfers and daily spa credits.
{/isPremiumMember}
If the field or value is blank, null, false, or 0, the block is hidden. If it has any other value it is shown.
Image placeholders
Replace a placeholder image with a dynamic image from Salesforce:
In the template, insert an image and set its alt text to {logoImage}. Pass the image as a base64 data URL in the merge data. Use a small Apex helper to convert a ContentVersion to base64 if needed.
Formatting tips
- Bold, italic, font size, and color applied to a placeholder character are applied to the merged value
- Do not split a placeholder across two formatting runs — type it as a single unformatted token, then format the whole token if needed
- Tables, text boxes, headers, and footers all support placeholders
Building a Flow with KS: Generate Document
You do not need to write a JSON string manually. Use Get Records elements with KS: Build Merge Context and KS: Add Table to assemble merge data without any code.
Simple example
Get Records → Contact (Id, FirstName, LastName, Level__c, Next_Check_in_Date__c)
│
▼
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 back, {Level__c} member.
We look forward to seeing you on {Next_Check_in_Date__c}.
Example: record with a related list
Add Get Records elements for the child records, then chain KS: Add Table to include each collection:
Get Records → Contact
Get Records → Booking__c (filter: Contact__c = recordId, all records)
│
▼
KS: Build Merge Context record → {!Get_Contact} → contextJson
KS: Add Table records → {!Get_Bookings} → contextJson
tableName → "bookings"
KS: Generate Document
templateKey → "welcome-packet"
contextJson → {!contextJson}
filename → "Welcome Packet.docx"
Template:
Dear {FirstName},
Your upcoming experiences:
{#bookings}
{Experience_Name__c} — {Date__c} — {Number_of_Guests__c} guests
{/bookings}
Multiple related lists
Chain as many KS: Add Table calls as needed — there is no limit:
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}
Template uses {#bookings}...{/bookings} and {#credits}...{/credits} as normal.
Generating a PDF output
Change the filename extension to .pdf — the template is still a .docx, but the output is converted automatically:
filename → "Welcome Packet.pdf"
PDF AcroForm Templates
Use KS: Fill PDF Form to fill fields in an existing PDF with interactive form fields (AcroForms). This is the right tool when you have a fixed-layout PDF form — such as a government form, registration card, or application template — and want to fill it from Salesforce data without any formatting setup.
How it works
- Create a PDF with AcroForm fields (in Adobe Acrobat, Nitro, or any tool that supports fillable PDFs)
- Name each form field to match the Salesforce API name of the field you want to fill
- Upload the PDF to Salesforce Files
- Build a Flow using Get Records → KS: Fill PDF Form
Keelstone reads the field names from the PDF, looks up matching values from the record, and fills each field. Any field in the PDF with no matching Salesforce field is left blank.
Naming convention
| PDF field name | Salesforce field filled |
|---|---|
FirstName | Contact.FirstName |
LastName | Contact.LastName |
Level__c | Contact.Level__c (custom field) |
Account.Name | Contact.Account.Name (relationship) |
Owner.Email | Contact.Owner.Email (relationship) |
Field names are case-sensitive. Relationship fields use dot notation — the PDF field Account.Name is filled from whatever value Account.Name resolves to on the queried record.
Flow setup
Get Records → Contact (Id, FirstName, LastName, Level__c, Account.Name)
│
▼
KS: Fill PDF Form
templateId → <your PDF template's ContentDocumentId>
record → {!Get_Contact}
filename → "Jane Doe Registration.pdf"
linkedEntityId → {!recordId}
flatten → true (makes the output non-editable; omit to keep fields editable)
Outputs
| Output | Description |
|---|---|
| Content Document ID | ContentDocumentId of the filled PDF in Salesforce Files |
| Document URL | Public download URL (when Generate External Link is true) |
| Success | Boolean — true if the form was filled successfully |
| Error Message | Error detail if Success is false |
When to use AcroForms vs. Word templates
AcroForms are the right choice when you have a fixed-layout form with a known, static set of fields — a government form, registration card, or application template. Each field in the PDF maps to exactly one Salesforce value.
Use KS: Generate Document with a .docx template instead when:
- You need a repeating table or list — AcroForms have no concept of looping. There is no equivalent of
{#contacts}...{/contacts}in a PDF form. Each field is a single named slot. - You need conditional sections that show or hide based on data.
- The number of rows in your data is dynamic.
For example, if you want to generate a document listing a collection of contacts with their first name, last name, and email, a Word template is the correct approach:
{#contacts}
{FirstName} {LastName} {Email}
{/contacts}
Use KS: Add Table with tableName = "contacts" to add the collection to the context, then pass contextJson to KS: Generate Document. The Word engine repeats the block for each contact. Output as .pdf if needed by setting the filename extension to .pdf.
Other limitations
- The PDF must not be password-protected.
- Checkbox fields: the PDF field value is set to
trueorfalsebased on the Salesforce field value.
Template Storage Best Practices
| Practice | Why |
|---|---|
| Create a Keelstone Template record for each template | Gives you a stable Template Key that works across orgs and doesn't break when files change |
| Use a descriptive, unique Template Key | The key is stored in the document — changing it later requires re-generating documents |
| Store static values in Template Fields | Keeps flows simple and ensures org-level values (resort name, phone) are consistent across all generated documents |
| Upload template revisions as a new file on the same template record | Keelstone uses the most recent file — the Template Key stays the same |
| Keep a backup copy outside Salesforce | Protects against accidental deletion |
| Test templates with representative data before deploying | Edge cases like very long names or empty fields can break layouts |
Frequently Asked Questions
What placeholder syntax does Keelstone use?
Single curly braces: {FieldApiName}. The field name must exactly match the Salesforce API name including __c for custom fields. This is different from some other merge tools that use double braces {{field}}.
What is a Template Key and why should I use it?
The Template Key is a stable developer slug (e.g. "welcome-packet") on the Keelstone_Template__c record. Using it instead of a ContentDocumentId means your flows work across orgs — the key stays the same even if you reinstall from a package or move to a new org. When you generate a document using a Template Key, Keelstone also embeds the key in the document's custom properties so the taskpane can show template-specific actions.
Can I use formulas in Excel templates? Yes. Formulas are preserved exactly. Placeholders only replace cell values — they do not affect formulas in other cells. If a formula references a merged cell, it recalculates based on the merged value.
What happens if a placeholder in the template has no matching field value?
The placeholder is replaced with an empty string. No error is thrown. To hide content when a value is absent, use a conditional block: {#fieldName}...{/fieldName}.
My placeholder isn't being replaced — what should I check?
- Confirm the API name in the template exactly matches the field queried in the flow (case-sensitive,
__csuffix included) - Confirm the field is included in the Get Records queried fields list — fields not queried are not available
- In Word, make sure the placeholder was typed as a single run — autocorrect or Track Changes can split it into multiple XML runs. Delete and retype it if in doubt
- Confirm the template file was re-uploaded after making changes
Can I use relationship fields like {Account.Name} in the template?
Yes. As long as the flow's Get Records element queries the relationship field (e.g. SELECT Name, Account.Name FROM Contact), you can reference it as {Account.Name} in the template. Dot notation works at any depth.
Can I password-protect templates? No — Keelstone must be able to read the file to merge it. Do not password-protect template files.
Can I generate a PDF instead of a Word document?
Yes. Set the filename input to end in .pdf (e.g. Welcome Packet.pdf). The template is still a .docx; the merge engine converts the output to PDF automatically.