keelstoneGenerateDocument
A managed package Flow Screen component that merges Salesforce data into an Office template (Excel, Word, PowerPoint, or PDF) and saves the result to Salesforce Files. It takes a session ID, template ID, merge data, and output filename as inputs — it does no Salesforce data fetching of its own.
Package: Keelstone Actions (managed, namespace kstone)
Component name: kstone:keelstoneGenerateDocument
Flow target: lightning__FlowScreen
Input properties
| Property | Type | Required | Description |
|---|---|---|---|
keelstoneSessionId | String | Yes | Keelstone session token. Pass {!KeelstoneSessionId} from the flow variable. |
templateKey | String | Yes | Developer key of a Keelstone_Template__c record. Resolves to the latest attached template file automatically. |
contextJson | String | Yes* | Merge context as a JSON string. Build with KS: Build Merge Context + KS: Add Table, a custom Apex class, or an LWC. *Required if no mergeData or template static fields supply data. |
mergeData | String | No | JSON string merged on top of contextJson. Keys here take priority. |
linkedEntityId | String | Yes | Record ID to attach the generated file to |
filename | String | Yes | Output filename including extension, e.g. "Account Report.xlsx" or "Proposal.docx". The extension determines the output format. |
externalLink | Boolean | No | If true, generates a public ContentDistribution URL. Default: false. |
Output properties
| Property | Type | Description |
|---|---|---|
keelstoneSessionId | String | Pass-through of the input session ID |
mergedFileId | String | ContentDocumentId of the generated output file |
fileLink | String | Internal Salesforce file URL, or public distribution URL if externalLink = true |
What it does
When the flow screen renders, the component runs automatically:
- Validates all required inputs are present
- Sends the template and merge data to the Keelstone server-side generation service, which downloads the template from Salesforce Files, merges the data in-memory, uploads the result to Salesforce Files linked to
linkedEntityId, and returns theContentDocumentIdof the generated file - If
externalLink = true, creates a public ContentDistribution URL and setsfileLinkto it - If
externalLink = false, setsfileLinkto the internal Salesforce file URL - Dispatches
FlowNavigationFinishEventto advance the flow
The output format is determined by the filename extension:
.xlsx— Excel workbook (template must be.xlsx).docx— Word document (template must be.docx).pptx— PowerPoint presentation (template must be.pptx).pdf— PDF converted from the merged template file
The composable pattern
This component is designed to be used alongside a custom data-fetching LWC that runs on the screen before it. The data-fetcher queries whatever Salesforce data is needed for the document and outputs it as mergeData JSON. This separation means keelstoneGenerateDocument works for any object, any data shape, and any template.
Quick Action → Screen Flow
│
├── Screen 1: Your custom data-fetcher LWC
│ @api (output) contextJson ← builds JSON from Apex query
│ @api (output) linkedEntityId ← the record to attach the file to
│
└── Screen 2: kstone:keelstoneGenerateDocument
keelstoneSessionId ← {!KeelstoneSessionId}
templateKey ← flow variable (set by admin)
contextJson ← from Screen 1 output
linkedEntityId ← from Screen 1 output
filename ← flow variable or formula
Template placeholder syntax
The keys in your mergeData JSON become the top-level namespaces in the template. For example:
{
"account": { "Name": "Acme Corp", "BillingCity": "San Francisco" },
"opportunities": [
{ "Name": "Renewal 2025", "Amount": 45000, "StageName": "Proposal" }
]
}
Corresponding Excel template cells:
{account.Name}
{account.BillingCity}
{#opportunities}
{Name} | {Amount} | {StageName}
{/opportunities}
See Template Variables for the full placeholder reference.
Building a custom data-fetcher LWC
Create a headless Flow Screen LWC that queries Salesforce and outputs mergeData. Here's a complete example for an Account with related Opportunities:
Apex controller
public with sharing class AccountDocDataController {
@AuraEnabled
public static Map<String, Object> getAccountData(Id accountId) {
Account acc = [
SELECT Id, Name, Phone, Website,
BillingStreet, BillingCity, BillingState, BillingPostalCode,
Owner.Name, AnnualRevenue, NumberOfEmployees
FROM Account
WHERE Id = :accountId
LIMIT 1
];
List<Map<String, Object>> opps = new List<Map<String, Object>>();
for (Opportunity o : [
SELECT Name, Amount, StageName, CloseDate, Owner.Name
FROM Opportunity
WHERE AccountId = :accountId
ORDER BY CloseDate DESC
LIMIT 50
]) {
opps.add(new Map<String, Object>{
'Name' => o.Name,
'Amount' => o.Amount,
'Stage' => o.StageName,
'CloseDate' => String.valueOf(o.CloseDate),
'Owner' => o.Owner.Name
});
}
return new Map<String, Object>{
'account' => acc,
'opportunities' => opps
};
}
}
LWC component
// accountDocData.js
import { LightningElement, api } from 'lwc';
import getAccountData from '@salesforce/apex/AccountDocDataController.getAccountData';
export default class AccountDocData extends LightningElement {
// Flow output variables — available to subsequent screens
@api contextJson = '';
@api linkedEntityId = '';
// Injected by the flow when running on a record page
@api recordId;
async connectedCallback() {
try {
const data = await getAccountData({ accountId: this.recordId });
this.contextJson = JSON.stringify(data);
this.linkedEntityId = this.recordId;
} catch (e) {
console.error('AccountDocData error:', e);
}
}
}
<!-- accountDocData.html — intentionally blank, this is a headless screen -->
<template></template>
<!-- accountDocData.js-meta.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>66.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__FlowScreen</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__FlowScreen">
<property name="recordId" type="String" label="Record ID" />
<property name="contextJson" type="String" label="Context JSON (output)"
role="outputOnly" />
<property name="linkedEntityId" type="String" label="Linked Entity ID (output)"
role="outputOnly" />
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
Flow configuration
In the Screen Flow builder:
Screen 1 — Add c:accountDocData:
- Set
Record IDinput →{!$Record.Id}(from the record page) - Capture outputs:
mergeData→ flow variablevarMergeData,linkedEntityId→ flow variablevarLinkedEntityId - Set Hide Footer = true (headless screen, no Next button shown)
Screen 2 — Add kstone:keelstoneGenerateDocument:
Keelstone Session ID→{!KeelstoneSessionId}Template Key→ flow variablevarTemplateKey(admin sets this)Context JSON→{!varContextJson}Linked Entity ID→{!varLinkedEntityId}Output Filename→ formula:{!$Record.Name} & " - Report.xlsx"
Quick action setup
- In Setup → Object Manager → Account → Buttons, Links, and Actions → New Action
- Action Type: Flow, Flow: select your flow
- Label:
Generate Report - Add to the Account page layout via Page Layouts or the Lightning App Builder
Error handling
| Scenario | Behavior |
|---|---|
| Any required input missing | Error state: specific message identifying which input |
| Template not found in SF Files | Server returns error → error state shown in component |
| Server merge fails | Error state with HTTP status code |
Output format mismatch (e.g., .docx template + .xlsx output) | Error state before server call |
| Merge limit exceeded (plan limit) | 429 error → error state with upgrade message |
All errors display inline — the flow screen remains open so the user can read the message.
Generating a public link
Set externalLink = true to create a publicly accessible URL for the generated file. This is useful for sharing documents with external recipients (e.g., customers receiving a proposal via email).
The public URL is created via ContentDistribution and does not require Salesforce login to access. Confirm your data classification policy permits public distribution of the content before enabling this option.
The fileLink output variable will contain the public URL when externalLink = true, or the internal Salesforce file URL when false.