First off, welcome to my Substack. I don’t imagine I’ll be writing with any sort of cadence or regularity, but while I previously posted this kind of content directly to LinkedIn, there are a number of reasons as to why I began to find that less than optimal, so I’m giving this a whirl.
This is my first proper CPQ-related article in nearly a year and a half. Part of that is because I had just put a lot of effort into producing my final word on designing complex pricing structures without code, but also, a number of changes happened in my personal and professional lives, and I needed a break. But duty/necessity has called, and it makes sense to put something together in a format that’s actually usable by other people!
With that navel-gazing out of the way, let’s talk Dynamic Bundles. But before getting into that specifically, first of all, what IS a “bundle” in CPQ, anyway, dynamic or otherwise? Contrary to popular belief, it has nothing to do with pricing - unless you want it to. A “bundle” in CPQ terms is the conceptual structure where you start with a “product” of some sort (or offering, or service, but the Salesforce object is named “Product”) and relate a bunch of stuff to it which can (optionally or otherwise) be associated to it at the time of quoting that product. What kind of stuff?
Other products - Defined as “Product Options” (each existing as a junction between 1 “parent” product and 1 “child” product, which itself may be a parent to other products), grouped by “Product Features” (junction between 1 parent product and many options), further grouped by “Feature Categories” (an optional picklist on each Product Feature record).
Attributes, which may be associated only to the parent product or a Product Feature in a particular bundle (called “Configuration Attributes”), or to one or many Product Options in many bundles (called “Global Attributes”).
Various CPQ capabilities I will collectively categorize as “business logic for configuration”. This includes functions that reference, change, select/create, remove/delete, enable, disable, show, hide, validate, or alert different elements/actions in different contexts. Note the “for configuration” part - this is bucketed *after* selecting the parent product, until finalizing the configuration of that product. This is separated from logic that determines both which bundle(s) is/are available to select, and defining any systemic pricing relating to valid and finalized configurations.
Stop here if you are not already familiar with these concepts, and go learn them - I recommend the superb Trailhead project here:
This is the standard structure for Salesforce CPQ, and it works well, but in certain situations, it may be overly cumbersome. For example:
Your bundle needs to contain over 700 Product Options, and there is no possible way to make it smaller through an alternate design.
You have a large product catalog, and you need to cascade attribute values from the parent down to all options, but you don’t have a lot of “rules” governing the selection of those options (or none at all).
You feel limited by the fact that Product Options can only be constrained by a rule or deleted; you have scenarios where options may need to be deactivated and then reactivated, such as to reflect an out-of-stock.
You are a reseller, and your product list is something that is outside of your control and updated frequently.
You aren’t using a feature that is not compatible with Dynamic Bundling, such as Evergreen Contracts.
Dynamic Bundling helps to handle all of those scenarios, because it allows the user to build their bundle as they are quoting, governed by a “looser” filtering logic. This way, a user could build a bundle with 5 child products from a list of 10,000, without an admin having to create 10,000 Product Options (and 1,000 Features, etc.), nor the user having to load all of that data as they are quoting. They are given a UX very similar to the top-level product search they used to select their bundle in the first place. I won’t rehash the basic steps to configure Dynamic Bundling here, which requires only a Product Feature with a particular configuration, and a Product Rule of a certain type targeting that feature…because not only does that have good documentation, but a Trailhead module that walks you through it as well, so check that out!
Once you have familiarized yourself with the existing content, here is a diagram that may help you to understand the data model in aggregate:
As you can see, “regular” and Dynamic Bundles share many elements, but not all. From a sales perspective, Dynamic Bundles allow product hierarchies to be defined without explicit definition of Product Options beforehand, by “filtering” the product catalog selections available through what’s made available to the user in the UI and through Filter Rules. CPQ then uses the user’s selections to define the parent/child hierarchy on the Quote itself, with parent and child Quote Lines (through the same self-lookup field regular bundles use, “Required By”).
So here we are, you’ve completed the Trailhead, reviewed the diagram, and thus know everything there is to know about Dynamic Bundling, right? The “delta” between what’s possible, and the Help docs + Trailhead, is that you can actually configure this feature so that different fields are used and shown compared to other areas of your bundle, product search, or even one Dynamic Bundle Feature vs. another…this is very dynamic, hence “Dynamic Bundling Squared”. But…there are a set of “quirks” for the configuration of this feature that, heretofore, were unaddressed/undocumented, relating to how the configuration elements involved interrelate:
Custom field sets per-Feature are able to be specified, with the filter fields being defined via the Feature picklist field “Dynamic Option Filter Field Set”. Simply create your field set, add the fields you want to use as filters, add the field set’s API name as a picklist value to the field, then define that value on the relevant Dynamic Bundle Feature. And you still can set those filter fields as optional or mandatory (“Default”) in your Product Filter Rule.
There is a 2nd custom field set which can optionally be set, specified in the field “Dynamic Option Lookup Field Set”. This defines the “columns” that will display in your filtered search result set. Here is where we encounter the 1st undocumented “quirk” - whether you stick with the default here or not, any field which you filter by, must be present in the default or specified custom field set, otherwise the “filtering” does not actually work.
Unfortunately, while at this point your Dynamic Bundle Feature should be working (after addressing bullet #2’s quirk), you will be aghast to find that your filter results are missing some or all of their field values in the UI! It turns out that you must *also* add the lookup fields to either the provided field set “Search Results” (which is used in “regular” product search), or a different one called “ConfiguratorLookup” - this is the default field set for Dynamic Bundling filtered search results (the one you are substituting in the 2nd bullet).
To summarize, if you want to use Dynamic Bundling, particularly if you have multiple Dynamic Bundle Features under the same parent product, or maybe even more so if you use this capability in multiple parent products, you will want to standardize to *always* using custom filter and lookup field sets, and *never* using the default ConfiguratorLookup field set in your UI, since you will almost certainly want a different set of fields for different Dynamic Bundle Features. By configuring it as-described, you can avoid including a bunch of extra fields meant for this or that Dynamic Bundle Feature in *every* Dynamic Bundle Feature, as well as avoid showing Dynamic Bundle-specific filtering fields in the top-level product search UI. Here is a diagram:
The downside is having to add your fields to 3 different field sets to configure the capability (in addition to creating the Product Filter Rule, and adding your Product fields to the Product Option object), but once you have it set up, it should be pretty maintenance free - what changes are the products that are returned as a result of filtering, which you can control by adding new ones (individually, or en masse via a dataloading tool), or by either deleting or excluding existing ones (by setting the Product or Price Book Entry/ies to inactive). Traditional bundles with Product Options are not this easy to modify; Product Options don’t have an active/inactive flag, and deleting a Product Option requires the CPQ admin to either A. Delete all Quote Lines referencing that Product Option first, or B. Enabling “Allow Option Deletion”, then deleting the options, then disabling the setting (else you enable yourself or others to delete options which shouldn’t be deleted), and the result is having a bunch of Quote Lines created from Product Options that no longer exist (which may or may not be important to you).
There is one other downside/risk which should be discussed. If you configure a lot of Dynamic Bundle Features with a lot of different fields, your ConfiguratorLookup field set is going to have a lot of fields on it, since it is at the core of enabling these Features as previously discussed. This, in of itself, is not a problem - There are no limits to the number of fields on 1 field set, and you’re not using it anyway, so who cares? The problem comes when you create a Dynamic Bundle Feature and forget to specify the custom field sets, or someone/some process comes along and removes the values specified. The end result is going to be a bundle which tries to load the default field set (including dozens, or potentially hundreds of fields). I would recommend both adjusting the FLS on those fields to ensure only a small number of people can make changes to them, and/or locking down the record access on existing Product Features, as well as creating a validation rule that prevents a Product Feature record from being saved if the fields are blank when the “Option Selection Method” is “Dynamic”. You could also deactivate or delete the default field set picklist values from those fields for added protection, but that alone is not enough, because CPQ will interpret a blank field value as the default field set. Here’s what my Validation Rule looks like for your reference:
That’s all admittedly a lot to take in. The best way to gain an understanding, in my view, is through an actual configured example. So what follows is a functional and technical explanation of a proof-of-concept scenario I created, along with relevant screen shots:
As a value-added reseller (VAR), I would like to enable sales reps to configure product bundles for our customers from 2 separate product lists (updated weekly), where the products share some attributes (but not all), so that I can quickly update the system to reflect current offerings with minimal downtime.
First things first, start with the definition of a parent product in your Salesforce instance. An appropriate product may not yet exist, because it’s very likely that a “virtual bundle” is the most appropriate choice here - a product which is not a marketable product or service on its own and possesses no value, but serves as a container for both user UI/rules and relationships between selections in a database sense. It would be like ordering a “pizza” and starting from scratch (i.e. not selecting “Meat Lovers” or a preconfigured option) - there is no definition or price for that pizza until you specify your size, crust, sauce, cheese, and toppings, at minimum. In my case, I am starting with a parent product called Test Bundle Parent, priced at $0 in my price book, and set up with the bare minimum of configuration (1st 3 bullets) to be recognized by CPQ as a bundle parent.
Since the functional specification indicates 2 separate product lists, I created 2 Product Features related to Test Bundle Parent, unimaginatively named Dynamic Bundle Feature 1 and Dynamic Bundle Feature 2. The “Option Selection Method” field needs to be set to “Dynamic” as you previously learned, but I will need to come back later for the remainder of the configuration of these Product Features.
I created 5 “Attribute” fields on the Product object as Text(255) fields to differentiate the Products which will filter into each Dynamic Bundle Feature. I named these “Attribute1” - “Attribute5”. Since these will serve as my filters, I also created duplicate fields on the Product Option object. And in real life, you most likely want to create twin fields onto your Quote Line object (and others as well).
Now it’s time for the field sets, on the Product object. I created one for filter fields and one for lookup fields, for both Features - TestFilter1 and TestLookup1 with Attributes 1, 2, and 4; and TestFilter2 and TestLookup2 with Attributes 2, 3, and 5. All four field sets also include “Product Name”, and all 5 attribute fields got added to the “ConfiguratorLookup” field set. Here is a view of the 4 field sets I created:
Here’s an example of one of those field sets, with only the fields I’m using:
Here is the standard field set, containing the fields I will use across all field sets:
5. Now I can finish configuring my Product Features, but first I need to add TestFilter1 and 2 as picklist values to the “Dynamic Option Filter Field Set” field, and TestLookup1 and 2 to the “Dynamic Option Lookup Field Set” field. Here is the end result:
6. Next up is the Product Filter Rules governing each feature. In a case where my actions would be the same, I could use the same rule for multiple Dynamic Bundle Features, but since I target different fields per-Feature, I need multiple rules. There is not much else that’s interesting there, if you have already completed the previously-referenced Trailhead module:
The rule setup for the 2nd Product Feature looks identical, except for the “Product Rule Name” (2 instead of 1), the Configuration Rule “Product Feature” value (2 instead of 1, again), and the Product Action “Filter Field” (testing Attribute 5 instead of 1 - you will need to add these field API names as picklist values, of course). The fact that these are configured as “Default Filters” are what will allow my Dynamic Bundle Feature lookup results to only contain the relevant products, regardless of what fields I present or what the user does.
7. (Optional) For my PoC, I did not want the child products being made available inside my Dynamic Bundle Features also being available as standalone selections in top-level Product Search. However, I can’t mark them as “Components”, else they won’t show up within the bundle, either. So I created a hidden Product Search Filter attached to the default “Add Products” Custom Action, ensured my parent product had a Product Code, ensured my child products did not, and checked for that value, as shown here:
8. “What child products?” you may be asking by now. That is my next and final configuration step, because I am finished with the “scaffolding” necessary for the setup, and now I can load, activate, and/or deactivate as few or as many products as I want via a data loading application which fits my criteria. I only need 4 products to adequately demo this, so that’s all I created:
I of course added them as Price Book Entries to the same Price Book that “Test Bundle Parent” is in, and in the same currency(ies).
9. What follows is the resulting UX:
Here’s my product search, only showing the bundle parent, because I filtered out my child products in #7, and I don’t have anything else in my quote’s price book. You can control the columns here through the “Search Results” field set.
Upon selection of my parent, here’s the result I see in configuration, with no options or attributes (because I didn’t configure any), just my 2 Dynamic Bundle Features.
Here’s the view when I click “Add Options” under “Dynamic Bundle Feature 1” (#’s 2 and 5). You will see the filter fields I defined in the “TestFilter1” field set (#4), the columns I defined in the “TestLookup1” field set, and the field values from my products in the lookup results because all of the fields present are in the “ConfiguratorLookup” field set. And I only see Test Products 1 and 3 (#8) because my Product Filter Rule (#6) has a default filter applied, looking for nonblank values in Attribute1 (#’s 3 and 8), which is not populated for Test Products 2 and 4.
Here’s the result of the filtering applied. I will add “Test Product 1” to my bundle.
Here’s the view when I click “Add Options” under “Dynamic Bundle Feature 2” (#’s 2 and 5). You will see the filter fields I defined in the “TestFilter2” field set (#4), the columns I defined in the “TestLookup2” field set, and the field values from my products in the lookup results because as before, all of the fields present are in the “ConfiguratorLookup” field set. And I only see Test Products 2 and 4 (#8) because my Product Filter Rule (#6) has a default filter applied, looking for nonblank values in Attribute5 (#’s 3 and 8), which is not populated for Test Products 1 and 3.
Here’s the result of the filtering applied. I will add “Test Product 4” to my bundle.
Here’s the end result in the configurator. You can also specify custom field sets per Product Feature here, using the field “Configuration Field Set”, and following steps similar to #’s 4 and 5 above.
And finally, here’s what it looks like in my Quote Line Editor. Note: “Campaign Name” and “Approval Level” are custom fields you can ignore for this PoC.
You can specify custom and dynamic field sets in this view based on the parameters of your choice, by creating the custom fields “EditLinesFieldSetName__c” and/or “HeaderFieldSetName__c”
I hope I haven’t “scared you away” from using Dynamic Bundle Features (or CPQ in general!) As you can now see, a CPQ admin must be very deliberate and cautious when configuring this feature to make it work, but it does enable a UX for both admins and users not otherwise possible, or very very laborious at best.
And lastly, if you like this functionality, but would like to see some of the arbitrary configuration steps simplified, I have linked a couple of Ideas for you to upvote and promote:
And, a bonus:
Nice explanation!