Creating Documents in Salesforce using Apex
Generate Viewer documents programmatically from Apex code using the Viewer invocable API. This guide covers everything from basic implementation to production-ready patterns with error handling and testing.
- Viewer package installed in your Salesforce org
- Basic knowledge of Apex and Salesforce objects
- At least one Viewer template created and available
Quick Overview
The Viewer Apex API allows you to:
- Generate documents on demand from any Apex code
- Trigger document creation from flows, batches, or scheduled jobs
- Integrate with custom processes like document management or archival
- Automate document delivery via email, file storage, or Slack
- Handle errors gracefully with proper exception handling

Setup: Prepare Your Template
Before writing Apex, create your document template in Viewer:
- Open Viewer → Templates
- Create or select an existing template
- Copy the Template ID from the URL:
viewer2__Template__crecord ID - Note the Primary Object (Account, Opportunity, etc.)
- Verify template renders correctly with test data
Finding Your Template ID
In Salesforce:
- Go to Viewer → Templates
- Open your template
- The URL shows:
/lightning/r/viewer2__Template__c/a0X... - The ID after
a0X...is your Template ID
Or query directly:
List<viewer2__Template__c> templates = [
SELECT Id, Name
FROM viewer2__Template__c
];
Basic Implementation
Simple Document Generation
public class BasicDocumentGeneration {
public static void createDocument(String recordId, String templateId) {
// Create request
viewer2.viewerInvocable2.Request req = new viewer2.viewerInvocable2.Request();
req.recordId = recordId; // Salesforce record (Account, Opportunity, etc.)
req.paramString = templateId; // Template ID from Viewer
// Execute request
List<viewer2.viewerInvocable2.Request> requests =
new List<viewer2.viewerInvocable2.Request>{ req };
List<viewer2.viewerInvocable2.Response> responses =
viewer2.viewerInvocable2.execute(requests);
// Check response
if (responses != null && responses.size() > 0) {
viewer2.viewerInvocable2.Response response = responses[0];
System.debug('Document generated: ' + response);
}
}
}
Output: Document generated and returned in response object
Production-Ready Implementation
Complete Example with Error Handling
public class ProductionDocumentGeneration {
/**
* Generate a document with comprehensive error handling
* @param recordId - The Salesforce record ID
* @param templateId - The Template ID from Viewer
* @return DocumentResult - Result object with success/error details
*/
public static DocumentResult createDocument(String recordId, String templateId) {
DocumentResult result = new DocumentResult();
try {
// Validate inputs
if (String.isBlank(recordId) || String.isBlank(templateId)) {
result.isSuccess = false;
result.errorMessage = 'Record ID and Template ID are required';
return result;
}
// Create request
viewer2.viewerInvocable2.Request req = new viewer2.viewerInvocable2.Request();
req.recordId = recordId;
req.paramString = templateId;
// Execute
List<viewer2.viewerInvocable2.Request> requests =
new List<viewer2.viewerInvocable2.Request>{ req };
List<viewer2.viewerInvocable2.Response> responses =
viewer2.viewerInvocable2.execute(requests);
// Process response
if (responses == null || responses.isEmpty()) {
result.isSuccess = false;
result.errorMessage = 'No response received from Viewer';
return result;
}
viewer2.viewerInvocable2.Response response = responses[0];
// Check for errors in response
if (response.isSuccess) {
result.isSuccess = true;
result.documentId = response.documentId;
result.documentUrl = response.documentUrl;
System.debug('Document created successfully: ' + response.documentId);
} else {
result.isSuccess = false;
result.errorMessage = response.errorMessage;
System.debug('Document generation failed: ' + response.errorMessage);
}
} catch (Exception e) {
result.isSuccess = false;
result.errorMessage = 'Error generating document: ' + e.getMessage();
System.debug(LoggingLevel.ERROR, 'Document generation exception: ' + e.getStackTraceString());
}
return result;
}
/**
* Result wrapper class
*/
public class DocumentResult {
public Boolean isSuccess = false;
public String documentId;
public String documentUrl;
public String errorMessage;
}
}
Usage Example
// Simple usage
String accountId = '001xx000003DH1';
String templateId = 'a0Xxx0000012AB0';
ProductionDocumentGeneration.DocumentResult result =
ProductionDocumentGeneration.createDocument(accountId, templateId);
if (result.isSuccess) {
System.debug('Document: ' + result.documentUrl);
} else {
System.debug('Error: ' + result.errorMessage);
}
Advanced Patterns
Bulk Document Generation
For processing multiple records:
public class BulkDocumentGeneration {
/**
* Generate documents for multiple records
*/
public static List<DocumentResult> createDocuments(
List<String> recordIds,
String templateId
) {
List<DocumentResult> results = new List<DocumentResult>();
List<viewer2.viewerInvocable2.Request> requests =
new List<viewer2.viewerInvocable2.Request>();
// Build all requests
for (String recordId : recordIds) {
viewer2.viewerInvocable2.Request req =
new viewer2.viewerInvocable2.Request();
req.recordId = recordId;
req.paramString = templateId;
requests.add(req);
}
// Execute all at once
List<viewer2.viewerInvocable2.Response> responses =
viewer2.viewerInvocable2.execute(requests);
// Process responses
for (viewer2.viewerInvocable2.Response response : responses) {
DocumentResult result = new DocumentResult();
result.isSuccess = response.isSuccess;
result.documentId = response.documentId;
result.errorMessage = response.errorMessage;
results.add(result);
}
return results;
}
public class DocumentResult {
public Boolean isSuccess;
public String documentId;
public String errorMessage;
}
}
Scheduled Document Generation
For nightly or periodic document creation:
/**
* Scheduled class for periodic document generation
* Example: Run nightly to generate invoices
*/
public class ScheduledDocumentGeneration implements Schedulable {
public void execute(SchedulableContext context) {
// Get all records that need documents
List<Opportunity> opps = [
SELECT Id
FROM Opportunity
WHERE IsClosed = true
AND DocStatus__c = null
LIMIT 100
];
String templateId = 'a0Xxx0000012AB0'; // Invoice template
List<String> oppIds = new List<String>();
for (Opportunity opp : opps) {
oppIds.add(opp.Id);
}
// Generate documents in bulk
List<ProductionDocumentGeneration.DocumentResult> results =
BulkDocumentGeneration.createDocuments(oppIds, templateId);
// Log results
Integer successCount = 0;
for (ProductionDocumentGeneration.DocumentResult result : results) {
if (result.isSuccess) {
successCount++;
}
}
System.debug('Generated ' + successCount + ' documents');
}
}
Schedule it:
// Run nightly at 2 AM
String scheduleExpr = '0 0 2 * * ?';
String jobId = System.schedule('Generate Invoices', scheduleExpr,
new ScheduledDocumentGeneration());
Use Cases
1. Document on Demand from Flow
Create a text input in Flow that calls this invocable Apex action:
public class DocumentGenerationAction {
@InvocableMethod(label='Generate Viewer Document')
public static List<DocumentOutput> generateDocument(List<DocumentInput> inputs) {
List<DocumentOutput> outputs = new List<DocumentOutput>();
for (DocumentInput input : inputs) {
ProductionDocumentGeneration.DocumentResult result =
ProductionDocumentGeneration.createDocument(
input.recordId,
input.templateId
);
DocumentOutput output = new DocumentOutput();
output.success = result.isSuccess;
output.documentUrl = result.documentUrl;
output.errorMessage = result.errorMessage;
outputs.add(output);
}
return outputs;
}
public class DocumentInput {
@InvocableVariable(required=true)
public String recordId;
@InvocableVariable(required=true)
public String templateId;
}
public class DocumentOutput {
@InvocableVariable
public Boolean success;
@InvocableVariable
public String documentUrl;
@InvocableVariable
public String errorMessage;
}
}
2. Auto-Generate on Record Creation
trigger OpportunityDocumentTrigger on Opportunity (after insert) {
// Only process closed won opportunities
List<Opportunity> closedWon = new List<Opportunity>();
for (Opportunity opp : Trigger.new) {
if (opp.IsClosed && opp.IsWon) {
closedWon.add(opp);
}
}
if (!closedWon.isEmpty()) {
// Call document generation asynchronously
List<String> oppIds = new List<String>();
for (Opportunity opp : closedWon) {
oppIds.add(opp.Id);
}
String templateId = 'a0Xxx0000012AB0'; // Contract template
System.enqueueJob(new DocumentGenerationQueueable(oppIds, templateId));
}
}
/**
* Queueable for async document generation
*/
public class DocumentGenerationQueueable implements Queueable {
private List<String> recordIds;
private String templateId;
public DocumentGenerationQueueable(List<String> recordIds, String templateId) {
this.recordIds = recordIds;
this.templateId = templateId;
}
public void execute(QueueableContext context) {
BulkDocumentGeneration.createDocuments(recordIds, templateId);
}
}
3. Email Documents to Users
public class EmailDocuments {
public static void sendDocumentEmail(String recordId, String templateId, String recipientEmail) {
// Generate document
ProductionDocumentGeneration.DocumentResult result =
ProductionDocumentGeneration.createDocument(recordId, templateId);
if (result.isSuccess) {
// Send email with document link
Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new String[] { recipientEmail });
email.setSubject('Your Document is Ready');
email.setPlainTextBody('Your document is ready to download:\n' +
result.documentUrl);
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { email });
} else {
System.debug('Document generation failed: ' + result.errorMessage);
}
}
}
Best Practices
- Always validate inputs - Check for null/blank IDs before API calls
- Use result wrappers - Create classes to consistently handle responses
- Handle exceptions - Wrap API calls in try-catch blocks
- Log comprehensively - Use System.debug() with log levels
- Test thoroughly - Minimum 75% code coverage for deployment
- Use async processing - For bulk operations, use Queueable or Batch classes
- Monitor governor limits - Each request counts against API limits
Governor Limits
The Viewer API respects Salesforce governor limits:
| Limit | Impact |
|---|---|
| API Calls | Each execute() counts as 1 API call |
| CPU Time | Document rendering uses CPU time |
| Heap Size | Large documents consume heap memory |
| Time Limits | Sync calls have 2s timeout, async has full execution window |
Recommendation: Use batch or queueable for bulk generation (100+ records).
Troubleshooting
Response is null or empty
Causes:
- API call failed silently
- Invalid template ID
- Record doesn't match template's primary object
Solution:
// Verify template exists and matches record type
viewer2__Template__c template = [
SELECT Id, Primary_Object__c
FROM viewer2__Template__c
WHERE Id = :templateId
];
// Verify record exists
SObject record = Database.query('SELECT Id FROM ' +
template.Primary_Object__c + ' WHERE Id = :recordId');
isSuccess = false but no error message
Cause: Viewer encountered an unhandled exception
Solution:
- Check Viewer logs in Salesforce
- Verify template contains valid field references
- Test template in Viewer UI with same record
- Check field-level security and sharing
Debug:
System.debug(LoggingLevel.DEBUG, 'Full response: ' + JSON.serialize(response));
"Record ID required" error
Cause: Empty or null recordId in request
Solution:
if (String.isBlank(recordId)) {
throw new IllegalArgumentException('Record ID cannot be blank');
}
req.recordId = recordId.trim(); // Remove whitespace
Testing Your Implementation
Unit Test Example
@isTest
public class DocumentGenerationTest {
@TestSetup
static void setupTestData() {
// Create test account
Account acc = new Account(Name = 'Test Account');
insert acc;
// Create test template
viewer2__Template__c template = new viewer2__Template__c(
Name = 'Test Template',
Primary_Object__c = 'Account'
);
insert template;
}
@isTest
static void testCreateDocument() {
Account acc = [SELECT Id FROM Account LIMIT 1];
viewer2__Template__c template = [SELECT Id FROM viewer2__Template__c LIMIT 1];
Test.startTest();
ProductionDocumentGeneration.DocumentResult result =
ProductionDocumentGeneration.createDocument(acc.Id, template.Id);
Test.stopTest();
System.assertEquals(true, result.isSuccess,
'Document should generate successfully');
System.assertNotEquals(null, result.documentId,
'Document ID should not be null');
}
@isTest
static void testCreateDocumentWithInvalidId() {
Test.startTest();
ProductionDocumentGeneration.DocumentResult result =
ProductionDocumentGeneration.createDocument('invalid', 'invalid');
Test.stopTest();
System.assertEquals(false, result.isSuccess,
'Should fail with invalid IDs');
System.assertNotEquals(null, result.errorMessage,
'Error message should be provided');
}
}
More resources
Additional Resources
- Test with your templates before production
- Implement error handling for your use case
- Consider async patterns for bulk operations
- Monitor Viewer logs for any issues