Skip to main content

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

PropertyTypeRequiredDescription
keelstoneSessionIdStringYesKeelstone session token. Pass {!KeelstoneSessionId} from the flow variable.
templateKeyStringYesDeveloper key of a Keelstone_Template__c record. Resolves to the latest attached template file automatically.
contextJsonStringYes*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.
mergeDataStringNoJSON string merged on top of contextJson. Keys here take priority.
linkedEntityIdStringYesRecord ID to attach the generated file to
filenameStringYesOutput filename including extension, e.g. "Account Report.xlsx" or "Proposal.docx". The extension determines the output format.
externalLinkBooleanNoIf true, generates a public ContentDistribution URL. Default: false.

Output properties

PropertyTypeDescription
keelstoneSessionIdStringPass-through of the input session ID
mergedFileIdStringContentDocumentId of the generated output file
fileLinkStringInternal Salesforce file URL, or public distribution URL if externalLink = true

What it does

When the flow screen renders, the component runs automatically:

  1. Validates all required inputs are present
  2. 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 the ContentDocumentId of the generated file
  3. If externalLink = true, creates a public ContentDistribution URL and sets fileLink to it
  4. If externalLink = false, sets fileLink to the internal Salesforce file URL
  5. Dispatches FlowNavigationFinishEvent to 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 ID input → {!$Record.Id} (from the record page)
  • Capture outputs: mergeData → flow variable varMergeData, linkedEntityId → flow variable varLinkedEntityId
  • Set Hide Footer = true (headless screen, no Next button shown)

Screen 2 — Add kstone:keelstoneGenerateDocument:

  • Keelstone Session ID{!KeelstoneSessionId}
  • Template Key → flow variable varTemplateKey (admin sets this)
  • Context JSON{!varContextJson}
  • Linked Entity ID{!varLinkedEntityId}
  • Output Filename → formula: {!$Record.Name} & " - Report.xlsx"

Quick action setup

  1. In Setup → Object Manager → Account → Buttons, Links, and Actions → New Action
  2. Action Type: Flow, Flow: select your flow
  3. Label: Generate Report
  4. Add to the Account page layout via Page Layouts or the Lightning App Builder

Error handling

ScenarioBehavior
Any required input missingError state: specific message identifying which input
Template not found in SF FilesServer returns error → error state shown in component
Server merge failsError 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.


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.