Dynamic CPQ Quote PDFs in Salesforce: Part 2 - Quote Template and Template Content

April 29, 2024
Tsenko Aleksiev
Claudia Schneebacher
Stefanie Peijan

How to create CPQ PDFs in Salesforce

Part 2 - Quote Template and Template Content

At this step, things are getting a bit tricky, but interesting at the same time. I know it’s going to look a bit confusing ( maybe even a bit hard at some point ), but bear with me, once we get the hang of it, you will see how easy it is.

First things first: click the App Launcher and type in Quote Templates, click it to go to the Quote Templates list view. There click New button and let’s give our template the name of “Bikes Unlimited Template”, set the template as Default and put it in Deployment Status “Deployed”, like this

You are done! You now have a basic Quote Template setup, which is ready for use. Don’t trust me? Check the template Related tab, it probably looks like this:

Let’s give it a test spin, huh? Go to a Quote which you created with a bike and it’s a add-ons, on the buttons, you should have “Preview Document” and “Generate Document”, either of those will work, but for now, click only preview, then Preview again


“Uhm, wait wait, that doesn’t look right…”, you might say, well duh, we haven’t added much info yet, but you have the structure.

Now, the real work begins, roll up your sleeves and let’s get this template done!

This is the template that the client gave us and we have to make him happy with the result, so let’s grab this logo and do the following:

On the upper right corner, click on your profile icon and in the drop – down menu, click “Switch to Salesforce Classic”.

What, do you think that Salesforce was Lightning all the time?

Click on the most right “+” sign to view all the tabs

and look for “Documents”

Let’s add our logo to the Quotes folder.

Click “New” to add the document itself.

IMPORTANT: don’t forget to add a description for the file and mark the file as “Externally Available Image”. Also, add the logo in a public folder like the Quotes for example.

In the “Select the File” section, click Choose File and add it from your local computer and click Save.

This should be the result for you:

NB about the file: Supported image file types for Quote Documents are JPG, PNG, BMP, and GIF. The size of the file should be as small as possible and must not exceed the 5-MB file size limit for Quote Documents.


Now, look at the URL, while you are still at the same page with the document uploaded, it should look something like this:

After the last “/”, do you see the ID – a good old 15 characters Salesforce ID, in my case this is “0152o0000078Gjn”, this is the unique ID of my logo. Copy that ID and head back to lightning by clicking “Switch to Lightning Experience”.

In our Quote Template, paste the Id of the logo in the “Logo Document Id” field

Now CPQ knows where exactly our logo is and how to find it. You still don’t trust me? Go back to that test quote and preview the document again

What do you have to say now? ☺

While we are here, in the “Page Information” section, set the “Top Margin” to 0, because we won’t need it in our template

Also, on the Related tab, remove all the line fields, so we have it clear, like this:

Ok, but we are still far away from our target result, right! In order to have something on our template and not use something default that is coming from Salesforce, let’s create some Content records.

IMPORTANT: Keep in mind that all the setup and styling on HTML markup and CSS is according to the size of my logo picture and you might need to adjust additionally to fit your needs!

Click the App launcher and look for “Template Content”

Click New and let’s start building our Header first. Check “HTML” and “Continue”

Let’s call our Content “BU Template Header” ( BU – Bikes Unlimited ) and set the Font Family to for example, Helvetica.

Click the Table symbol

and add 1 row 2 columns table, Alignment Right, like this

Now, we are going to use the company logo merge field like this “{!companyLogo}” ( you can read about Quote Template merge fields here https://help.salesforce.com/s/articleView?id=sf.cpq_merge_fields.htm&type=5 ), but we want to change the styling of the table a bit, so write your HTML markup, combined with inline CSS like mine. Click the Source button and enter the same markup

This is the result:

Let’s see what happened to our template if we try to preview it, head back to our Quote and click Preview Document

Not cool at all… Keep in mind that if something goes wrong with the template, you are most probably going to see this error … which is not that helpful so it is a good idea to test the preview more often and catch what broke at the moment of building it, otherwise it is hard to debug this ( I am speaking from personal experience here )

For the current error, we might say that the reason is the empty template, the system will not allow us the generate ( or even preview ) an empty document – we deleted the Line fields, remember.

Ok, let’s fix this! Go to the Template Content list again and create a new one, HTML again and give it the name “BU Cover Page Content”. For now, add only the Quote Name merge field on the canvas “{!quote.Name}”, like this and click Save.

Go to the Quote Template Related and click New on the Section line

For the new section, give the name “BU Cover Page” and set Top Margin to 5, Display Order to 10 and click the Content we created in the previous step. Save and go preview the document again!

This time we don’t have an Error, we see the logo has moved to the top right corner of the document, the name of our company is in the top left corner and the Quote name is automatically displayed beneath it.

We are making progress, let’s keep it up!

We want to print our company’s name and address, so go on the Quote Template, to the “Corporate Information” section and add some data in there, click Save after you are done.

Now, go to the BU Cover Page Content and right before the {!quote.Name} merge field, add a new table with 2 columns and 3 rows, aligned to the left

and populate the table like this:

If you recall our options to use merge fields, we have the !template option, which is picking up data from the template itself and each of those SBQQ fields is the field we populated with our company’s info. If you are wondering how I got those field names, you have 2 options:

1. In the Setup, go to the Object Manager and look for the Quote Template object, there you can find all the fields on the object

2. If you are using Google Chrome, I would recommend installing Salesforce Inspector Reloaded ( https://chromewebstore.google.com/detail/salesforce-inspector-relo/hpijlohoihegkfehhibggnkbjhoemldh ) - it’s an amazing free tool that in my opinion is something every Salesforce specialist should have. With it, while still on our template details tab, you can click the Arrow on the right of your screen ( you will see it after installing the extension ) and click “Show fields API names”

Now really, how cool is that! You can thank this amazing person for that extension – Thomas Prouvot ( https://www.linkedin.com/in/thomasprouvot/ )! Thank you, Thomas! ☺

Going back to our work – go to the Quote and click … guess what – Preview!

There we go, we now have our company info there and we know how to update it if needed. But the space between the logo and the company info is a bit too much, isn’t it.

On the Quote Template, Related, click Edit on the “BU Cover Page” Section is and give it Top Margin of 2, save and go preview the result again

Much better!

But … ( what, you thought we are done already?! ) we want our company name to be bigger and eye – catching with the company color, which is #538587.

Go to the “BU Cover Page Content”, click Edit and mark the line with the company’s name and click the Text Color icon

In the pop-up window, populate the hexadecimal color code in the cell and click Ok

With the text still marked, click the Font Size icon and give it the size of 16, save and preview the result … I think you should know where that would be by now ☺

Perfect! Amazing job so far!

Let’s add some more data on the Quote – go to the Address Information section and populate the fields

Now, in the Template Content, add another table beneath the table with the company’s info, 7 rows, 3 columns, aligned left

Place the Quote Name in the first cell and populate the rest like this with some styling on it

You guessed right, we are again picking up data from the Quote and this is the result

And we are done with this particular Section. Let’s create a new Template Content for the products added to the Quote.
Choose the type to be “Line items” and I will call mine “BU Products Content”, the table style will be Standard.

Create a new Section, let’s call it “BU Products Table” and set it like this:

Top Margin – 0
Display Order – 20
Content – BU Products Content

Keep With Previous – Auto

Keep Together – Auto

Now, on the Quote Template, the Line Columns, click New and set these values

Column Heading – Description

Display Order – 10

Field Name – SBQQ__ProductName__c ( Salesforce Inspector Realoaded -> Show all data on the Quote Line ;) )

Click save and preview the document

Looking better and better! Let’s add some more Quote Line Items fields


Unit Price



And the result

Ok, on the template content add some company color for the table ( without ‘#’ symbol ) by populating the value in the Text Color cell


Now comes the tricky part. I have a requirement to print conditionally text, based on the combination of the base product ( the bikes ) and the additional product options chosen. This is the part where I really love Salesforce, because it gives so many options to implement a solution and the end goal can be achieved in many different ways.

Our goal is:

When the base product is
Road Bike OR Racing Bike


Accessory is  Bottle holder

print this text: Our bottle holders are designed to fit seamlessly on road bikes and racing bikes. The adjustable design ensures a secure fit on various frame shapes and sizes, providing a hassle-free solution for cyclists across different disciplines.

When the base product is



Accessory is  Bottle holder

print this text: Our bottle holders are exclusively designed to meet the unique demands of E-Mountain biking. The form factor is optimized for E-Mountain bike frames, providing a secure and integrated solution for carrying your hydration.

Regardless of the base product, if accessory is

First aid kit

print this text: Crafted from high-quality and water-resistant materials, our first aid kit is built to withstand the demands of various cycling environments. Whether you're navigating off-road trails on an E-Mountain bike or cruising on smooth roads, our kit is designed to keep your essential medical supplies secure and dry.

When the base product is



Motor is

Hub Motor: Experience a smooth and responsive ride with our hub motors, integrated seamlessly into the wheel hub. Hub motors are known for their simplicity and ease of maintenance, making them an excellent choice for various riding conditions.

Mid-Drive Motor: Our mid-drive motors are strategically positioned at the bike's bottom bracket, offering a balanced and natural feel. These motors excel in challenging terrains, providing efficient power transfer directly to the chain for enhanced climbing and overall performance.

Performance Line: Elevate your riding experience with the Performance Line CX motor. Renowned for its powerful assistance, this mid-drive motor features responsive torque sensing, ensuring optimal support on any trail.

When the base product is



Battery is

Li-ion Battery: Our standard lithium-ion batteries are a tried-and-true choice for e-mountain biking. Known for their excellent energy density, Li-ion batteries provide a reliable and efficient power source, ensuring a consistent and enjoyable riding experience. They strike a balance between weight, capacity, and longevity, making them a popular choice among riders.

Li-Po Battery: For those seeking a compact and lightweight solution, our lithium polymer batteries are an excellent choice. Li-Po batteries, with their polymer electrolyte, offer a high energy-to-weight ratio. This design makes them suitable for riders looking to maximize power in a sleek and lightweight package, ensuring an exhilarating ride without compromising on performance.

21700 Battery: Elevate your e-mountain biking experience with the 21700 battery. Renowned for its higher energy density, this battery type provides increased capacity and efficiency, translating to longer ride times and improved overall performance. The 21700 cells are at the forefront of battery technology, delivering a powerful and reliable energy source for your adventures.

NCR18650GA Battery: Our e-mountain bike batteries also feature the trusted NCR18650GA cells. These 18650 lithium-ion cells are known for their high capacity and stable performance. Riders can expect a robust and consistent power supply, ensuring they can conquer challenging terrains with confidence and reliability.

As I mentioned, there are different options on how to achieve this and yes it’s hard, but we can do it. We have the power of Salesforce in our hands!

Let’s think about the problem and what are our options:

Approach A)

Create 10 different checkboxes on the Quote level, each representing the different combination that we want to print on the template.

Create a 10 different Template Content records and 10 different Template Sections, each holding the unique text that we want to print.

On every Template Section, there is a field, called “Conditional Print Field”. We can use this for every section, to check the respected checkbox on the Quote. In other words – check if “Print_Bottle_Holder_Road_Racing_Bike__c” ( sample checkbox on Quote ) is true and if yes, print the text for that combination, being hold in the current Template Content.

In order to populate dynamically those 10 checkboxes, we can create a record – triggered flow, that is being triggered on Quote Line create or update and for example check if this formula is true as an entry condition

Crazy, huh! If this formula returns true, holding every option that we have, we can add a decision element with 10 different paths for every separate option, based on the result, the flow will update the checkbox on the Quote and blah blah, you know the rest.

It seems like a lot of work, doesn’t it. Well it sure is, we have a complex requirement and the solution will not be easy. I am a big fan of the clicks-not code approach and I always start thinking about the solution in the following steps:

  • Can I achieve this with out-of-the-box automations and configurations
  • If yes, how complicated it is, how long will it take me to do it, how much effort, is it easy to maintain
  • If no, how complex will it be to do it with Apex, is it going to be longer to implement, is it going to be hard to maintain

I always strive to think about how hard will it be to update it in the future and how hard it will be if someone else needs to do it, not me.

And thinking about it, I came up with

Approach B)

Create a Custom metadata type, holding my texts to print, thus everyone can go and easily update the text itself without braking the rest of my logic.

Create an Apex controller that is going to collect the data that I need, do  some logic over it and pass data.

Create a Visualforce page using special markup, called XSL-FO (https://w3schools.sinsixx.com/xslfo/xslfo_reference.asp.htm), that is going to visualize the data, passed from my controller.

Seems a lot less steps to me, only once I am going to create 10 records and the next person after me, no matter an admin or a developer, will be able to update the text in one place.

Decision is made, let’s start building!

IMPORTANT: I am going to use the Developer Console, because I don’t want to make anyone install Visual Studio Code or Code builder at the moment. If you have any additional tool for writing code, you are welcome to use it.

Go to Setup and look for the “Custom Metadata Type”, create a new type, I am going to call mine, “Quote Dynamic Text”

I will create two custom fields, one for holding the combination between parent product and product option, and one for holding the text itself, like this

These are my records

And this is how one of those records looks like

We are done here! Now, if someone wants to update the description of any of the options, can do it on that record OR create a new one.

Let’s move to the Apex controller. Open the developer console and create a new Apex class

I will call mine “QuoteTemplate_DynamicText_Controller” and this is my controller

Create a Visualforce page, I will call mine “BU_Dynamic_Text_Content” and this is my markup ( remember about XSL-FO markup, right )

Ok, we are done. Now I have to create my Template Content. I want it to be of type “Custom” and I will call it “BU Dynamic Content”.

In the “Custom Source” field, I will have to point to the API name of my Visualforce page.

You can find that in Setup – Visualforce Pages and look for the name we gave it

This is how the content will look like

VERY IMPORTANT: the format of the Custom Source is
“/apex/c__” + API name of the Visualforce page, so in my case

Ok, let’s create a Template Section to hold our content, this is mine

Again, margins and everything additional is in respect with my document example, you will most probably need to adjust yours depending on the requirements.

One last part – Footer Content

Let’s create our last Template Content, type HTML and I will call it “BU Template Footer”.

Add a table from the symbol, 3 rows 1 column, left align. Populate the text from the example and click Source button

I have added additional color and the bottom border for the first cell, this is the result

Drums here!!! We are done! We have the full structure and everything needed! Let us test it

I know we have build this … but it really looks good, doesn’t it!

I hope you liked everything we did up until now! I know I am happy, now Bikes Unlimited can send Quotes to their customers and their templates look professionally styled! They also have the control to update the text and add additional records based on the combination of bike – accessory ☺

Decoration IconLet's get started Image
Are you ready?

Let's get started

Ready to unclutter your customer relations and digital pipelines? Whether quick chat or detailed discussion: we would love to hear from you. To work out what you really need – and if we’re the right fit for you – we offer a trial day.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Update cookies preferences