Skip to main content

Retrieve Data Using Apex Code with Viewer

Supply custom or external data to your Viewer templates by writing an Apex class that implements the Salesforce Callable interface. Viewer calls your class at render time and merges the returned JSON into the template alongside standard Salesforce record data.

Prerequisites
  • Viewer package installed in your Salesforce org
  • Basic knowledge of Apex and Salesforce objects
  • At least one Viewer template created and configured

How It Works

When a document is generated, Viewer can call a designated Apex class and pass it context information (record ID, template options). Your class runs any logic you need — a SOQL query, a callout to an external API, a calculation — and returns a JSON-serializable Map. Viewer then makes those values available in your Handlebars template just like any other field.

Example: External data fetch

Setup Overview

  1. Write an Apex class that implements Callable and returns a Map<String, Object>.
  2. Configure the template — set the External Data Class Name field to your Apex class API name.
  3. Reference the data in your template using the keys your class returns.

Step 1 — Write the Apex Class

Your class must implement the Callable interface. Viewer invokes getData on your class and expects a JSON-serializable return value.

GetDataPluginExt.cls
global with sharing class GetDataPluginExt implements Callable {

public Object call(String action, Map<String, Object> args) {
Map<String, Object> input = (Map<String, Object>) args.get('input');
Map<String, Object> output = (Map<String, Object>) args.get('output');
Map<String, Object> options = (Map<String, Object>) args.get('options');
return invokeMethod(action, input, output, options);
}

private Object invokeMethod(
String methodName,
Map<String, Object> inputMap,
Map<String, Object> outMap,
Map<String, Object> options
) {
switch on methodName {
when 'getData' {
// Example: fetch from an external REST API
String apiUrl = 'https://dummyjson.com/products/'
+ String.valueOf(Integer.valueOf(Math.round(Math.random() * 30)));

HttpRequest req = new HttpRequest();
req.setEndpoint(apiUrl);
req.setMethod('GET');
req.setHeader('Content-Type', 'application/json');
req.setTimeout(120000);

HttpResponse res = new Http().send(req);
Map<String, Object> result =
(Map<String, Object>) JSON.deserializeUntyped(res.getBody());

if (result == null) return null;
return result;
}
when else {
return null;
}
}
}
}
What to return

Return a Map<String, Object> (or any JSON-serializable object). Viewer merges the top-level keys into the template data context, so a map like {'productName': 'Widget', 'price': 99} lets you write {{productName}} and {{price}} directly in your template.

Keep it lean

Apex callouts and queries inside a synchronous document generation call are subject to Salesforce governor limits. Avoid heavy computation or large result sets — retrieve only what the template actually needs.


Step 2 — Configure the Template

  1. Open ViewerTemplates and open your template.
  2. Find the External Data Class Name field in the template settings panel.
  3. Enter the Apex class API name — for example, GetDataPluginExt.
  4. Save the template.

Viewer will call getData on that class whenever this template is rendered.


Step 3 — Use the Data in Your Template

Once the class is configured, the keys returned by your Apex method are available as Handlebars variables in your Word or HTML template:

Product: {{title}}
Price: ${{price}}
Brand: {{brand}}

Common Pattern: Returning SOQL Data

You can use Apex to run complex SOQL queries that are not possible directly in template syntax:

RelatedRecordsPlugin.cls
global with sharing class RelatedRecordsPlugin implements Callable {

public Object call(String action, Map<String, Object> args) {
Map<String, Object> input = (Map<String, Object>) args.get('input');
return invokeMethod(action, input);
}

private Object invokeMethod(String methodName, Map<String, Object> input) {
switch on methodName {
when 'getData' {
// Read the record ID passed in by Viewer
String recordId = (String) input.get('recordId');

List<Map<String, Object>> items = new List<Map<String, Object>>();
for (OpportunityLineItem oli : [
SELECT Name, Quantity, UnitPrice, TotalPrice
FROM OpportunityLineItem
WHERE OpportunityId = :recordId
]) {
items.add(new Map<String, Object>{
'name' => oli.Name,
'quantity' => oli.Quantity,
'unitPrice' => oli.UnitPrice,
'totalPrice' => oli.TotalPrice
});
}

return new Map<String, Object>{ 'lineItems' => items };
}
when else {
return null;
}
}
}
}

In your template, iterate with {{#each}}:

{{#each lineItems}}
{{name}} — {{quantity}} x ${{unitPrice}}
{{/each}}

Best Practices

Key Guidelines
  • Always return a valid JSON-serializable objectnull responses are silently ignored by Viewer.
  • Use with sharing — respect Salesforce record-level security in your class.
  • Handle null results defensively — check that API responses or SOQL lists are not empty before building your return map.
  • Write Apex tests — Salesforce requires at least 75% code coverage for deployment; mock HTTP callouts with HttpCalloutMock.
  • Avoid heavy processing — keep the data fetch focused on what the template needs to stay within governor limits (CPU time, callout limits, heap size).

Troubleshooting

Template renders blank where external data fields should appear

Causes:

  • The External Data Class Name field is empty or set to the wrong API name.
  • The class returned null.
  • The key name in your map does not match the template placeholder exactly (case-sensitive).

Check:

  1. Verify the class API name in the template settings.
  2. Add a System.debug in your class and view the debug log to confirm the return value.
  3. Confirm your template placeholder {{myKey}} matches the map key 'myKey' exactly.
Callout fails or times out

Causes:

  • The external endpoint is not added to Remote Site Settings or Named Credentials.
  • The request timeout is too low for the endpoint's response time.

Solution:

  • Go to Setup → Remote Site Settings and add the endpoint domain.
  • Increase req.setTimeout() (max 120,000 ms).
  • Consider caching the result in a Custom Metadata or Platform Cache record for frequently used data.
Governor limit errors during document generation

Cause: The Apex class is performing too many queries or callouts in a single execution.

Solution:

  • Reduce the number of SOQL queries — use a single query with relationships instead of multiple queries.
  • Retrieve only the fields your template actually uses.
  • For heavy operations, consider pre-computing data and storing it on the record before triggering document generation.