January 8, 2026

How to Build Custom LWC Sections for Document Builder in Revenue Cloud

Jean-Michel Tremblay

Salesforce Consultant

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__ServiceDocument target
  • 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:

  1. Apex Controller — Accepts Quote ID, queries Quote Line Items and Attributes, returns structured data
  2. LWC JavaScript — Wire adapter and data processing
  3. LWC HTML Template — Renders the data in the document
  4. 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.

  1. Open Document Builder — From Setup, navigate to Document Builder under Revenue Cloud
  2. Edit Your Template — Select your quote template and click Edit
  3. Find Custom Section — In the Components panel, look for your custom component with the masterLabel you defined
  4. Drag to Template — Drag the custom section onto your template canvas
  5. 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.

ScenarioWhat to Verify
Quote with configurable productsAttributes display correctly under each line item
Quote with standard products“No configuration attributes” message appears
Quote with mixed productsBoth types render properly
Quote with many line itemsPage 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

IssueCauseSolution
Component not appearing in Document BuilderMissing lightning__ServiceDocument targetAdd the target to your meta.xml and redeploy
No data displayedrecordId not being passedEnsure you’re using @api recordId (not @api quoteId)
Apex error in PDFPermission issuesEnsure users have access to the Apex class and queried objects
Styling looks different in PDFCSS not PDF-optimizedUse pt for fonts, avoid complex CSS features
Content cut off on page breaksLarge fixed-height containersAvoid 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__ServiceDocument target 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.

Leave a Comment

Free Assessment