One Lead record type, and the values it never offered
Builder prospects and homeowner service requests share a single Lead record type, told apart by a Customer Type field. The decision is sound and it is not free: picklist values the field had and the record type never enabled, REQUIRED_FIELD_MISSING on a builder insert, a conversion flow about to turn a builder's office into a job site, and an Opportunity that lands on the wrong record type.
A new builder prospect is going in. The rep opens the Lead Role dropdown to mark him as the builder’s purchasing agent, and the option is not there. Neither is Project Manager. Neither is Designer. The field has every one of them. The dropdown does not.
Nothing is broken, exactly. It is the first bill for a decision that I still think is right: builder prospects and homeowner service requests share one Lead record type. This post is the rest of that bill, line by line, because each line is a thing that one record type quietly makes your job instead of the platform’s.
The decision
At Doorvana, new-construction sales
starts with outreach to home builders, and the prospecting board for it lives on Lead. The
handoff note for that build leaned towards a separate NC_Prospect Lead record type. The
call that came back was the opposite: no second record type. Builder prospects stay on the
existing Service_Lead record type and are split out by Customer_Type__c = Builder.
By the test this org uses for every record type — can you name the process that
differs? — that is correct. There is one Lead business
process, Service Lead Process, and builders move through the same statuses as homeowners.
The process that really differs is the bid, and that starts after conversion, on the
New_Construction Opportunity record type. Before conversion, “builder” is something the
customer is. That is a field.
The record type’s own description now says so, because the label no longer does:
force-app/main/default/objects/Lead/recordTypes/Service_Lead.recordType-meta.xml
<RecordType xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Service_Lead</fullName>
<active>true</active>
<businessProcess>Service Lead Process</businessProcess>
<description>The only Lead record type. Service and replacement requests plus builder
prospects for new construction; builders are told apart by Customer_Type__c = Builder,
not a record type.</description>
<label>Service Lead</label>
<!-- … -->
</RecordType> “Service Lead” is now a misleading label for a record type that also holds builder prospects. It has not been renamed yet. A label-only change is the safe version; changing the developer name touches every flow, profile and permission set that references it.
Line one: the values the record type never offered
Lead_Role__c is a restricted picklist. Its value set already defined Project Manager,
Designer, Builder Representative and Purchasing Agent, for exactly this kind of contact. None
of them had ever been enabled on Service_Lead.
That is how record types work, and it is easy to forget when an object has only one. A record
type carries its own picklistValues list for every picklist on the object, and a record of
that type is offered those values and no others. Adding a value to the field does not add it
to the record type. With one record type the list is invisible, because it never disagrees
with anything you can see — until the record type starts serving a kind of record nobody had
in mind when its list was last edited.
The fix is four <values> entries:
force-app/main/default/objects/Lead/recordTypes/Service_Lead.recordType-meta.xml
<picklistValues>
<picklist>Lead_Role__c</picklist>
<values>
<fullName>Architect</fullName>
<default>false</default>
</values>
+ <values>
+ <fullName>Builder Representative</fullName>
+ <default>false</default>
+ </values>
<values>
<fullName>Business Contact</fullName>
<default>false</default>
</values>
<values>
<fullName>Commercial Tenant</fullName>
<default>false</default>
</values>
+ <values>
+ <fullName>Designer</fullName>
+ <default>false</default>
+ </values>
<!-- … Project Manager and Purchasing Agent added the same way … -->
</picklistValues> There is a quieter cost in the same file. Picklist defaults are set per record type, so
everything sharing a record type shares its defaults. On Service_Lead, Lead_Role__c
defaults to Homeowner. That is right for the service desk, and it is the value a builder
prospect starts with too unless something sets it.
Line two: required means required for everyone
The acceptance script inserts five fabricated builder prospects. The first run did not get as far as the board:
Both fields are <required>true</required> in their field definitions. That is a
service-intake rule: every homeowner enquiry must say how it arrived and what it is about.
Field-level required applies on every path, Apex and flows included, and on every record of
every kind. Share the record type and you share the rule.
Neither field is meaningless for a builder, so the answer was to give them honest values, not
to relax the rule for the service desk. A builder prospect is Outbound Call and
New Construction:
scripts/apex/prospecting-board-acceptance.apex
Lead newProspect(String company, String first, String last, String phone) {
return new Lead(
FirstName = first, LastName = last, Business_Name__c = company, Company = company,
Customer_Type__c = 'Builder', Lead_Role__c = 'Builder Representative',
LeadSource = 'Outbound - Drive By', Phone = phone,
// Universally required on Lead (a service-intake rule); builder prospects answer it this way.
Lead_Channel__c = 'Outbound Call', Request_Type__c = 'New Construction'
);
} New Construction being available on Service_Lead at all is the result of an earlier
fix, when that value was missing from the
record type and a new-construction enquiry could not be saved by any route.
Line three: the office that nearly became a job site
This is the one that would have hurt.
Lead_Convert_Build_Property_And_Household is a record-triggered flow on Lead that runs after
save when IsConverted becomes true. It builds what standard conversion cannot: it creates or
matches a Location from the lead’s address, links it to the household Account with a stock
AssociatedLocation, and stamps Opportunity.Location__c so the Field Service chain has a
property to hang a Work Order on.
For a homeowner, the lead’s address is the property. For a builder, it is the builder’s
office. Run unchanged on a builder lead, the flow would have created a property Location at
the office, linked it to the builder’s Account, and stamped it onto the bid as its job site —
the place a crew would eventually be sent to install doors.
Nothing would have errored. The record would have looked complete. The fix is one more condition on the existing decision:
force-app/main/default/flows/Lead_Convert_Build_Property_And_Household.flow-meta.xml
<decisions>
<description>… A Builder lead's address is the builder's office, not a job site, so
builder prospects never create a property Location or AssociatedLocation; the job site
arrives later, on the Opportunity.</description>
<name>Has_Service_Address</name>
<!-- … -->
<conditions>
<leftValueReference>$Record.Customer_Type__c</leftValueReference>
<operator>NotEqualTo</operator>
<rightValue>
<stringValue>Builder</stringValue>
</rightValue>
</conditions> Acceptance case 4 converts a builder and checks the result directly: Opportunity.Location__c
is null, no new Location exists at the office street address, and the builder’s Account has
no AssociatedLocation.
Line four: the Opportunity lands on the wrong record type
Lead conversion creates the Opportunity with the converting user’s default Opportunity
record type. Lead conversion field mapping — LeadConvertSettings — maps field values; it does
not pick the record type. For a rep whose default is Service, a converted builder produces a Service
Opportunity on the transactional process, which is the wrong pipeline, the wrong stages and
the wrong forecast.
The same after-save flow now checks for a converted builder first, then moves the Opportunity:
force-app/main/default/flows/Lead_Convert_Build_Property_And_Household.flow-meta.xml
<rules>
<name>Builder_With_Opportunity</name>
<conditionLogic>and</conditionLogic>
<conditions>
<leftValueReference>$Record.Customer_Type__c</leftValueReference>
<operator>EqualTo</operator>
<rightValue><stringValue>Builder</stringValue></rightValue>
</conditions>
<conditions>
<leftValueReference>$Record.ConvertedOpportunityId</leftValueReference>
<operator>IsNull</operator>
<rightValue><booleanValue>false</booleanValue></rightValue>
</conditions>
<connector><targetReference>Get_New_Construction_Record_Type</targetReference></connector>
<label>Yes</label>
</rules> It looks up the New_Construction record type by DeveloperName, never by a hard-coded ID,
and sets RecordTypeId on the converted Opportunity. Changing the record type is not enough
by itself, because the stage came from the old process. A formula keeps a stage that already
belongs to the Bid process and turns anything else into Discovery:
force-app/main/default/flows/Lead_Convert_Build_Property_And_Household.flow-meta.xml
<formulas>
<description>Keeps a stage that already belongs to the Bid process; anything else
(a Transactional stage from the default record type) becomes Discovery.</description>
<name>Bid_Stage</name>
<dataType>String</dataType>
<expression>IF(OR(TEXT({!Get_Converted_Opportunity.StageName}) = "Discovery",
TEXT({!Get_Converted_Opportunity.StageName}) = "Working - Bid",
TEXT({!Get_Converted_Opportunity.StageName}) = "Delivered - Bid",
TEXT({!Get_Converted_Opportunity.StageName}) = "Closed Won",
TEXT({!Get_Converted_Opportunity.StageName}) = "Closed Lost"),
TEXT({!Get_Converted_Opportunity.StageName}), "Discovery")</expression>
</formulas> Case 4 asserts both: RecordType.DeveloperName = 'New_Construction' and a Bid stage.
What one record type moved, and where to
| What a second record type would have done | Where it lives with one record type |
|---|---|
| Its own picklist value list | Every value enabled on Service_Lead’s picklistValues |
| Its own layout | A Dynamic Forms field section with a component visibility rule on Customer_Type__c |
| Its own picklist defaults | Shared defaults, set by automation where they do not fit |
| An obvious branch in automation | An explicit Customer_Type__c condition in every flow that cares |
| Required fields scoped to it | Required for everyone, answered honestly per kind |
The Lightning record page’s “Prospecting” section is the second row in practice:
force-app/main/default/flexipages/Lead_Record_Page_Service_lead.flexipage-meta.xml
<componentName>flexipage:fieldSection</componentName>
<identifier>flexipage_fieldSection_prospecting</identifier>
<visibilityRule>
<criteria>
<leftValue>{!Record.Customer_Type__c}</leftValue>
<operator>EQUAL</operator>
<rightValue>Builder</rightValue>
</criteria>
</visibilityRule> None of that is an argument for the second record type. A second record type would have
added a recordTypeVisibilities entry to every profile and permission set, a record type to
choose or derive on every new lead, and a way for a builder to be filed under the wrong one. It would
also have left lines one, three and four exactly where they were. The automation audit is
needed either way, and the picklist list just moves to a different file.
What one record type does is take the differences out of the place the platform enforces them
and put them in fields and flows, where only your tests enforce them. That is a good trade
when the process really is the same. Make it knowing the tests are now the enforcement. The
acceptance run for the board is 35 checks across five fabricated cases, and case 4 — convert a
builder, then read back the Opportunity’s record type, its stage, its Location__c and the
Account’s AssociatedLocation count — is what keeps lines three and four paid.