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 doneWhere it lives with one record type
Its own picklist value listEvery value enabled on Service_Lead’s picklistValues
Its own layoutA Dynamic Forms field section with a component visibility rule on Customer_Type__c
Its own picklist defaultsShared defaults, set by automation where they do not fit
An obvious branch in automationAn explicit Customer_Type__c condition in every flow that cares
Required fields scoped to itRequired 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.

Filed under

Disclosure: Doorvana's owner is also a co-owner of Nocona Consulting. This is a real engagement between two separate companies, but not an arm's-length one — and not an independent reference. More on the work page.

FAQ

Why is a picklist value missing from a Salesforce record even though the field has it?

Because the record's record type does not make that value available. A record type carries its own picklistValues list for every picklist on the object, and only those values are offered on records of that type. Adding a value to the field, or to its global value set, does not add it to existing record types. Check the RecordType metadata for a picklistValues entry for that field and enable the value there.

Should builder leads and homeowner leads have separate Lead record types?

Only if the Lead process differs: different Lead statuses, a different business process, or genuinely different handling before conversion. If both kinds of lead are worked through the same statuses and the real difference starts after conversion, one Lead record type plus a field such as Customer Type is simpler. The difference then belongs on the Opportunity, where a separate record type can drive a different set of stages.

Which record type does lead conversion give the new Opportunity?

Standard lead conversion creates the Opportunity with the converting user's default Opportunity record type. Lead conversion field mapping copies field values; it does not set the record type. If certain leads must convert into a particular Opportunity record type, something has to change it afterwards, for example a record-triggered flow on Lead that runs when IsConverted becomes true and updates ConvertedOpportunityId.

What does REQUIRED_FIELD_MISSING mean when inserting a Lead from Apex or a flow?

A field marked required in its field definition was left blank on the insert. Field-level required applies on every path, including Apex, flows and the API, not only on the page layout. When one Lead record type serves several kinds of lead, a field that is required for one kind is required for all of them, so every intake path has to supply a sensible value.

If a picklist value is enabled on a record type, can it have a different default for different kinds of record?

No. The default is set once per record type, per picklist. When two kinds of record share a record type they share its picklist defaults as well. A default that only suits one kind has to be removed, or set by automation that checks a field such as Customer Type.

Working on something like this?

I can tell you what it takes.

If you run a home services business and this looks like a problem you have, book a call and we will talk through it.