Document Builder in Salesforce Revenue Cloud provides excellent out-of-the-box capabilities for generating quote PDFs. However, there are scenarios where the standard components simply can’t display the data you need—particularly when working with child records of child records, or when you need complex formatting.
In this tutorial, I’ll show you how to build a custom Lightning Web Component (LWC) section that integrates seamlessly with Document Builder. Our specific example will display Quote Line Item Attributes—the configuration details for configurable products—but the technique applies to any custom data you need to show in your quote documents.
By the end of this guide, you’ll be able to:
- Create an Apex controller to fetch data for document generation
- Configure an LWC to work with Document Builder’s
lightning__ServiceDocumenttarget - Build HTML templates that render properly in PDF output
- Style your component for professional print output
- Deploy and integrate your custom section into Document Builder templates
The Problem: Standard Component Limitations
Document Builder’s standard Quote Line Items component is useful, but it has significant limitations:
- Fixed Columns — You cannot customize which fields appear in the standard Quote Line Items table
- No Child Record Access — Cannot display data from child objects of Quote Line Item (like Quote Line Item Attribute)
- No Rate Card Display — For usage-based products, there’s no way to show pricing tiers
- Limited Formatting — Complex nested data structures require custom rendering logic
While the Related Record Table component offers more flexibility for Quote Line Items, it still can’t access grandchild objects like Quote Line Item Attributes.
Our Use Case: Quote Line Item Attributes
When selling configurable products in Revenue Cloud (see Dynamic Attributes), customers make configuration selections that are stored as Quote Line Item Attributes. These are critical details that should appear on quote documents.
The data hierarchy is: Quote → Quote Line Item → Quote Line Item Attribute
Standard Document Builder components can’t traverse this three-level hierarchy. That’s where custom LWC sections come in.
The Solution: Custom LWC Sections
Document Builder supports custom Lightning Web Component sections that can be added to your templates just like standard components. These custom sections can:
- Query any data using Apex
- Display complex nested structures
- Apply custom formatting and styling
- Implement business logic for conditional display
The key to making an LWC work with Document Builder is the lightning__ServiceDocument target in your component’s meta XML file.
Architecture Overview
Our custom section consists of four main pieces:
- Apex Controller — Accepts Quote ID, queries Quote Line Items and Attributes, returns structured data
- LWC JavaScript — Wire adapter and data processing
- LWC HTML Template — Renders the data in the document
- LWC CSS — Print-optimized styles for PDF output
When Document Builder generates a PDF, it passes the Quote record ID to your LWC via the @api recordId property, your LWC calls the Apex controller, and the rendered HTML is converted to PDF.
Prerequisites
Before building your custom LWC section, ensure you have:
- Document Builder Enabled — Follow the setup steps in my Document Builder basics tutorial
- Salesforce DX Environment — VS Code with Salesforce Extension Pack
- Basic LWC Knowledge — Familiarity with Lightning Web Components, @wire adapters, and Apex integration
- Test Data — Quotes with configurable products that have Quote Line Item Attributes populated
Step 1: Build the Apex Controller
First, we create an Apex controller that retrieves Quote Line Items and their associated Attributes.
public with sharing class QuoteLineItemAttributeController {
@AuraEnabled(cacheable=true)
public static QuoteDataWrapper getQuoteWithLineItemAttributes(Id quoteId) {
QuoteDataWrapper wrapper = new QuoteDataWrapper();
wrapper.lineItems = new List<LineItemWrapper>();
// Query Quote Line Items
List<QuoteLineItem> lineItems = [
SELECT Id, Product2.Name, Quantity, ListPrice, UnitPrice, TotalPrice
FROM QuoteLineItem
WHERE QuoteId = :quoteId
ORDER BY LineNumber ASC
];
// Get all Quote Line Item IDs for attribute query
Set<Id> lineItemIds = new Set<Id>();
for (QuoteLineItem qli : lineItems) {
lineItemIds.add(qli.Id);
}
// Query Quote Line Item Attributes
Map<Id, List<QuoteLineItemAttribute__c>> attributesByLineItem =
new Map<Id, List<QuoteLineItemAttribute__c>>();
for (QuoteLineItemAttribute__c attr : [
SELECT Id, QuoteLineItemId__c, Attribute__r.Name, Value__c, Sequence__c
FROM QuoteLineItemAttribute__c
WHERE QuoteLineItemId__c IN :lineItemIds
ORDER BY Sequence__c ASC
]) {
if (!attributesByLineItem.containsKey(attr.QuoteLineItemId__c)) {
attributesByLineItem.put(attr.QuoteLineItemId__c,
new List<QuoteLineItemAttribute__c>());
}
attributesByLineItem.get(attr.QuoteLineItemId__c).add(attr);
}
// Build wrapper objects
for (QuoteLineItem qli : lineItems) {
LineItemWrapper liWrapper = new LineItemWrapper();
liWrapper.id = qli.Id;
liWrapper.productName = qli.Product2.Name;
liWrapper.quantity = qli.Quantity;
liWrapper.listPrice = qli.ListPrice;
liWrapper.unitPrice = qli.UnitPrice;
liWrapper.totalPrice = qli.TotalPrice;
liWrapper.attributes = new List<AttributeWrapper>();
if (attributesByLineItem.containsKey(qli.Id)) {
for (QuoteLineItemAttribute__c attr :
attributesByLineItem.get(qli.Id)) {
AttributeWrapper attrWrapper = new AttributeWrapper();
attrWrapper.id = attr.Id;
attrWrapper.attributeName = attr.Attribute__r.Name;
attrWrapper.attributeValue = attr.Value__c;
liWrapper.attributes.add(attrWrapper);
}
}
wrapper.lineItems.add(liWrapper);
}
return wrapper;
}
// Wrapper Classes
public class QuoteDataWrapper {
@AuraEnabled public List<LineItemWrapper> lineItems;
}
public class LineItemWrapper {
@AuraEnabled public Id id;
@AuraEnabled public String productName;
@AuraEnabled public Decimal quantity;
@AuraEnabled public Decimal listPrice;
@AuraEnabled public Decimal unitPrice;
@AuraEnabled public Decimal totalPrice;
@AuraEnabled public List<AttributeWrapper> attributes;
}
public class AttributeWrapper {
@AuraEnabled public Id id;
@AuraEnabled public String attributeName;
@AuraEnabled public String attributeValue;
}
}
Note: Adjust the field API names to match your org’s schema. The QuoteLineItemAttribute__c object may have different names in your implementation.
Step 2: Configure the LWC Meta XML
The meta XML file is critical—it tells Salesforce that this LWC can be used in Document Builder.
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__ServiceDocument</target>
<target>lightning__RecordPage</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__ServiceDocument">
<supportedFormFactors>
<supportedFormFactor type="Large" />
</supportedFormFactors>
</targetConfig>
</targetConfigs>
<masterLabel>Quote Line Item Attributes</masterLabel>
</LightningComponentBundle>
Critical: The lightning__ServiceDocument target is what makes your LWC appear in Document Builder’s Custom Section component. Without this, your component won’t be available for selection.
Step 3: Create the LWC JavaScript
The JavaScript file handles data retrieval and processing.
import { LightningElement, api, wire, track } from 'lwc';
import getQuoteWithLineItemAttributes from
'@salesforce/apex/QuoteLineItemAttributeController.getQuoteWithLineItemAttributes';
export default class QuoteLineItemAttributes extends LightningElement {
@api recordId;
@track quoteData;
@track error;
@wire(getQuoteWithLineItemAttributes, { quoteId: '$recordId' })
wiredQuoteData({ error, data }) {
if (data) {
this.quoteData = data;
this.error = undefined;
} else if (error) {
this.error = error;
this.quoteData = undefined;
}
}
get hasData() {
return this.quoteData &&
this.quoteData.lineItems &&
this.quoteData.lineItems.length > 0;
}
get processedLineItems() {
if (!this.quoteData || !this.quoteData.lineItems) {
return [];
}
return this.quoteData.lineItems.map(item => ({
...item,
hasAttributes: item.attributes && item.attributes.length > 0,
quantityDisplay: this.formatNumber(item.quantity, 0),
listPriceDisplay: this.formatCurrency(item.listPrice),
unitPriceDisplay: this.formatCurrency(item.unitPrice),
totalPriceDisplay: this.formatCurrency(item.totalPrice)
}));
}
formatNumber(value, decimals = 2) {
if (value === null || value === undefined) return '-';
return Number(value).toLocaleString('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
});
}
formatCurrency(value) {
if (value === null || value === undefined) return '-';
return Number(value).toLocaleString('en-US', {
style: 'currency',
currency: 'USD'
});
}
}
Step 4: Build the HTML Template
The HTML template defines how the data renders in your document. The template iterates over each Quote Line Item and displays its attributes.
Step 5: Add Print-Optimized CSS
CSS for Document Builder must be optimized for PDF output:
- Use pt (points) for fonts: More reliable in PDF than px or em
- Avoid hover/focus states: Not applicable in static PDF
- Use light backgrounds: Heavy colors may not print well
- Set explicit widths: Ensures consistent layout across pages
- Test with actual PDF generation: Screen preview can differ from final PDF
Step 6: Deploy to Your Org
Deploy your Apex class and LWC to your Salesforce org using Salesforce CLI:
# Deploy everything at once
sf project deploy start --source-dir force-app
Step 7: Add to Document Builder Template
With your LWC deployed, you can now add it to your quote document template.
- Open Document Builder — From Setup, navigate to Document Builder under Revenue Cloud
- Edit Your Template — Select your quote template and click Edit
- Find Custom Section — In the Components panel, look for your custom component with the masterLabel you defined
- Drag to Template — Drag the custom section onto your template canvas
- Save and Activate — Save and ensure it’s activated as the org default
Tip: The preview in Document Builder will show your custom component rendering with live data. Use this to verify your layout before generating actual PDFs.
Testing Your Custom Section
Thorough testing is essential before using custom LWC sections in production.
| Scenario | What to Verify |
|---|---|
| Quote with configurable products | Attributes display correctly under each line item |
| Quote with standard products | “No configuration attributes” message appears |
| Quote with mixed products | Both types render properly |
| Quote with many line items | Page breaks work correctly |
| Quote with no line items | “No line items found” message displays |
Other Use Cases
The same technique works for many other scenarios:
- Usage-Based Pricing Tiers — Display rate cards showing pricing tiers for usage products
- Product Specifications — Show detailed technical specifications from custom objects
- Discount Breakdowns — Create detailed tables showing how discounts were calculated
- Terms & Conditions — Dynamically include different T&C sections based on products
- Approval History — Show the approval chain for quotes that went through approval
- Custom Calculations — Display complex calculated values
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Component not appearing in Document Builder | Missing lightning__ServiceDocument target | Add the target to your meta.xml and redeploy |
| No data displayed | recordId not being passed | Ensure you’re using @api recordId (not @api quoteId) |
| Apex error in PDF | Permission issues | Ensure users have access to the Apex class and queried objects |
| Styling looks different in PDF | CSS not PDF-optimized | Use pt for fonts, avoid complex CSS features |
| Content cut off on page breaks | Large fixed-height containers | Avoid fixed heights; let content flow naturally |
Summary
Custom LWC sections unlock the full potential of Document Builder by allowing you to display any data in your quote documents. The key points are:
- Meta Config:
lightning__ServiceDocumenttarget enables Document Builder integration - @api recordId: Document Builder automatically passes the Quote ID to your component
- Apex Controller: Query any data structure and return it to your LWC
- @wire Adapter: Reactively fetch data when the component loads
- Print CSS: Optimize styling for PDF output (pt units, no hover states)
- Handle Nulls: Always account for missing or empty data gracefully
With this technique, you’re no longer limited by Document Builder’s standard components. Any data accessible via Apex can be displayed in your quote documents.