Skip to main content

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.

Prerequisites
  • 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

Creating document flow (Preview)

Setup: Prepare Your Template

Before writing Apex, create your document template in Viewer:

  1. Open ViewerTemplates
  2. Create or select an existing template
  3. Copy the Template ID from the URL: viewer2__Template__c record ID
  4. Note the Primary Object (Account, Opportunity, etc.)
  5. Verify template renders correctly with test data
Finding Your Template ID

In Salesforce:

  1. Go to ViewerTemplates
  2. Open your template
  3. The URL shows: /lightning/r/viewer2__Template__c/a0X...
  4. 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

BasicDocumentGeneration.cls
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

ProductionDocumentGeneration.cls
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:

BulkDocumentGeneration.cls
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:

ScheduledDocumentGeneration.cls
/**
* 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:

DocumentGenerationAction.cls
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

OpportunityDocumentTrigger.cls
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

EmailDocuments.cls
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

Key Guidelines
  • 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:

LimitImpact
API CallsEach execute() counts as 1 API call
CPU TimeDocument rendering uses CPU time
Heap SizeLarge documents consume heap memory
Time LimitsSync 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

DocumentGenerationTest.cls
@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


Next Steps
  • 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