Record Type Strategy At Scale
This skill activates when a practitioner is dealing with record type proliferation across objects that have accumulated many profiles, business processes, or picklist overrides. It provides patterns for rationalizing record types, managing the quadratic layout assignment problem, and migrating toward Dynamic Forms where supported.
Before Starting
Gather this context before working on anything in this domain:
- How many record types exist per object and how many profiles exist in the org? The layout assignment count is N record types multiplied by M profiles, so even modest growth creates quadratic complexity.
- Are practitioners assuming that record types are the only way to control field visibility? Dynamic Forms (Lightning App Builder component visibility filters) can replace many record-type-driven layout differences without creating new record types.
- The constraint that bites is not a hard limit, it is the operational cost of maintaining N record types x M profiles layout assignments and the picklist matrix underneath them. Use the count guide in
admin/record-types-and-page-layouts (1-4 healthy, 5-8 monitor, 9-12 likely over-built, 13+ redesign) as the operating threshold. UNVERIFIED (2026-09-04): the frequently quoted "200 record types per object" ceiling does not appear in the extracted Metadata API Developer Guide, Object Reference, or Salesforce App Limits cheat sheet; do not quote a number to a client without checking the current limits documentation for their edition.
- Which personas take record type access from a profile and which take it from a permission set group. The answer changes what you can render:
PermissionSetRecordTypeVisibility carries recordType and visible only, so the default record type and every layout assignment stay on the profile.
Questions to Ask Before Configuring
Ask these before the first retrieve. Each one maps to a failure this package has seen; an agent that skips them produces a matrix that deploys cleanly and governs nothing.
| Ask | Why it matters | What a good answer adds |
|---|
| "Which personas hold which profile, and which record types do they get from a permission set instead?" | The default flag and every layoutAssignments entry can only live on Profile; a permission set carries visible and nothing else | The grant-carrier column of the matrix, and the list of profiles that must still be edited (gotchas #5, #6) |
| "On each object, which single record type is the default for each persona?" | A persona with no profile default falls back to the Master record type, which applies no picklist filtering, and nothing errors | Exactly one default=true per (object, persona), verified with the describe audit rather than assumed (gotchas #5) |
| "How many records sit on each record type we intend to retire, and which picklist values do they hold?" | Reassignment blanks any value absent from the target type, and the update itself runs every validation rule, duplicate rule, and flow on the object | The at-risk row count, the value union to deploy first, and the automation inventory for the load (gotchas #7) |
| "Will this consolidation deactivate record types, and how many profiles and permission sets reference them?" | Inactive record types stop being retrieved or deployed inside profiles and permission sets, so the next diff shows mass deletions with no stated cause | A pre-deactivation snapshot commit and a split deploy plan (gotchas #8) |
| "Where is the record type inventory for this audit coming from?" | RecordType does not support the * wildcard, so a wildcard manifest returns zero record types and the audit reads clean on a sprawling org | A generated member list from a RecordType query, plus a non-zero assertion on it (gotchas #9) |
| "For each candidate merge, is the difference field visibility, or picklist values and business process?" | Only the first is removable by a FlexiPage; the second is what a record type is for | The consolidation shortlist, separated from the types that must survive |
| "What are N and M today, and which is cheaper to reduce?" | The cost is the product, so retiring profiles improves every object at once while retiring record types improves one | A prioritised target list, and an honest answer about whether the real project is profile consolidation |
What a proper configuration adds over just doing it: the matrix becomes a reviewable artefact that the profile and permission set XML is generated from, every persona has a governed default instead of a silent fall back to Master, and the retirement of a record type is a planned three-deploy sequence with a verified record migration rather than a delete that reassigns thousands of rows by owner profile.
Core Concepts
The N x M Layout Assignment Problem
Every record type on an object must have a page layout assigned for every profile in the org. If an object has 8 record types and the org has 50 profiles, that is 400 layout assignment cells to manage. Adding one record type adds 50 new assignments; adding one profile adds 8. This grows quadratically and is the primary driver of record type sprawl pain. The matrix is stored as repeated layoutAssignments elements inside each .profile-meta.xml — ProfileLayoutAssignments, with a required layout and an optional recordType — so it deploys per profile, and PermissionSet has no equivalent field to move it to. The real per-profile row count is N + 1, because the entry with no recordType is the fallback rule that applies when nothing matches.
Dynamic Forms as a Layout Multiplier Reducer
Dynamic Forms, available in Lightning App Builder, allows field-level visibility rules on a single page layout rather than requiring a separate layout per record type. A visibility filter can show or hide fields based on record type, field values, permissions, or device form factor. This means one flexible page can replace several static layouts. However, Dynamic Forms is not available on all standard objects — check compatibility before planning a migration. UNVERIFIED (2026-09-04): the per-object support list is a Salesforce Help topic and help.salesforce.com is not fetchable from this environment; confirm the current object list in Lightning App Builder for the target org before committing to a consolidation, and see admin/dynamic-forms-migration for the migration itself.
The Assignment Matrix Is the Artefact
At scale the thing you govern is not any one record type, it is the (object, record type, persona) table with visible, default, and layout columns. Author that table first; generate the XML from it. Three properties of the platform make the table, rather than the files, the right unit of review:
| Column | Carrier | Why it cannot move |
|---|
visible | Profile or PermissionSet | Both types have recordTypeVisibilities (API 29.0+ on permission sets) |
default | Profile only | PermissionSetRecordTypeVisibility has exactly two fields, recordType and visible — there is no default to set |
| layout | Profile only | layoutAssignments is a field of Profile; PermissionSet has none |
A blank default column for a persona is not a neutral state. The user falls back to Master, documented in Schema.RecordTypeInfo.isMaster() as "the default record type that's used when a record has no custom record type associated with it" — and Master filters no picklist values. The worked table, both rendered fragments, and the generator are in references/metadata-examples.md.
The Audit Surface Is Describe, Not SOQL
RecordType is queryable (SobjectType, DeveloperName, IsActive, BusinessProcessId, IsPersonType) and so are record counts per RecordTypeId. The visibility matrix is not: there is no standard object exposing which record types a profile or permission set grants. UNVERIFIED (2026-09-04): RecordTypeVisibility does not appear in the extracted Object Reference, so a SOQL-based visibility audit cannot be promised; the Setup path (Object Manager > object > Record Types, and Page Layouts > Page Layout Assignment) is the manual fallback. What does answer the question is Schema.DescribeSObjectResult.getRecordTypeInfos() evaluated as the running user — isAvailable(), isDefaultRecordTypeMapping(), isActive(), isMaster() per record type. Run it once per persona; it is the only check that reads the matrix the way a user experiences it.
Record Type ID Portability
Record Type IDs are org-specific 18-character Salesforce IDs. They are not stable across sandboxes and production. Code or configuration that hardcodes a Record Type ID will break on deployment. The canonical Apex pattern for resolving Record Type IDs at runtime is Schema.SObjectType.Account.getRecordTypeInfosByDeveloperName().get('Enterprise').getRecordTypeId(). In metadata (flows, validation rules), use $Record.RecordType.DeveloperName rather than a literal ID. In formulas, use RecordType.DeveloperName comparisons.
Common Patterns
Pattern 1: Consolidate Record Types, Differentiate with Dynamic Forms
When to use: An object has 5+ record types where the differences are primarily field visibility rather than distinct business processes or picklist value sets.
How it works:
- Audit existing record types and catalog the actual differences (fields shown, picklist values, business process).
- Identify record types that share the same business process and picklist values but differ only in field layout.
- Merge those record types into one, retaining the picklist and business process definition.
- Build a Dynamic Forms page in Lightning App Builder with component visibility rules to show or hide fields based on a controlling field or the remaining record type.
- Union the retired types' picklist values onto the surviving record type and deploy that, plus the merged layout, first — this deploy is additive and reversible.
- Migrate existing records using Bulk API 2.0 to update RecordTypeId to the consolidated record type, while the old types are still active. Records must not point at a type you are about to deactivate.
- Only then deactivate the retired record types and prune their
recordTypeVisibilities and layoutAssignments rows from every profile and permission set — keeping the fallback assignment that names no record type.
Why not the alternative: Keeping separate record types solely for field visibility means every new profile multiplies the layout assignment burden. Dynamic Forms eliminates that multiplier for field-visibility-only differences.
Pattern 2: Business Process Alignment
When to use: Record types have drifted from their original business process intent and picklist values are inconsistent across record types on the same object.
How it works:
- Export RecordType metadata XML for the object. Each record type references a BusinessProcess and contains picklistValues overrides.
- Map each record type to its actual business meaning (e.g., "Enterprise Sale" vs. "SMB Sale" on Opportunity).
- Normalize picklist values so that each record type's overrides reflect the real business process, not historical accidents.
- Remove record types that represent the same business process under different names.
- Redeploy the cleaned metadata using Metadata API or a change set.
Decision Guidance
| Situation | Recommended Approach | Reason |
|---|
| Field visibility is the only difference between record types | Consolidate record types and use Dynamic Forms | Eliminates N x M layout explosion for field-only differences |
| Record types drive distinct picklist value sets or business processes | Keep separate record types | Picklist filtering and business process (Sales Process, Support Process) require distinct record types |
| Record Type IDs are referenced in Apex or Flows | Replace with DeveloperName-based lookups | IDs are org-specific and break across environments |
| Object is not Dynamic Forms compatible | Use fewer record types with broader layouts | Cannot rely on Dynamic Forms; minimize layout assignments manually |
| Org has 50+ profiles and growing | Migrate to permission sets and reduce profile count | Fewer profiles directly reduces the M in the N x M equation |
Recommended Workflow
- Build the inventory the manifest cannot. Run
SELECT SobjectType, DeveloperName, IsActive FROM RecordType and the per-object counts grouped by RecordTypeId. Generate the RecordType members for your manifest from that result — RecordType takes no wildcard, and a starred manifest returns zero of them with no error (gotchas #9). The generator is in references/metadata-examples.md §6.
- Retrieve the whole matrix in one manifest. Objects, layouts, record types, and every profile and permission set together. Retrieving a record type or layout changes what the profiles in the same package contain, so a narrow retrieve produces profiles missing assignments they actually have. Commands and order:
references/metadata-examples.md §7.
- Lint what came back.
python3 skills/admin/record-type-strategy-at-scale/scripts/check_record_type_strategy_at_scale.py --manifest-dir force-app/main/default --profile-count <M>. Clear every ERROR (hardcoded Ids; a profile that grants record types on an object and defaults none of them) before designing anything; triage the WARNs (orphan record types, record types with no layout assignment anywhere, objects at 13+).
- Write the target matrix. Use the table shape in
references/metadata-examples.md §1 — one row per (object, record type, persona), plus the fallback row with no record type. Mark the grant carrier per cell. Anything that changes here must survive the questions above, especially the single default=true per (object, persona).
- Render the matrix into XML and stage three deploys, not one. Deploy (a) the merged layout and unioned picklist values, then (b) nothing — the data migration runs alone — then (c) deactivations plus the pruned profiles and permission sets. Splitting them is what keeps the deactivation diff reviewable (gotchas #8) and closes the window where a user can pick a type with no layout.
- Run the data migration as a data project. Preflight the at-risk picklist values, size the automation that will fire on every row (
gotchas.md #7), run a sandbox sample, read failedResults, then run production. Queries, CSV shape, sf data update bulk, and the raw Bulk API 2.0 ingest job are in references/metadata-examples.md §4.
- Verify per persona, not per file. Re-run the checker, run the two verification queries in
references/metadata-examples.md §8 (retired types inactive, zero records left on them), and run the Apex describe audit in §5 as each persona — exactly one isDefaultRecordTypeMapping(), and it must not be the Master row.
Review Checklist
Run through these before marking work in this area complete:
Salesforce-Specific Gotchas
Non-obvious platform behaviors that cause real production problems:
- Master record type is always available — The "Master" record type cannot be deleted and is always present. If a user's profile has no other record type assigned, they default to Master, which shows all picklist values with no filtering. This silently breaks picklist governance when profiles are misconfigured.
- A permission set grants visibility but can never grant the default or the layout —
PermissionSetRecordTypeVisibility has exactly two fields, recordType and visible (API 29.0+). The default flag lives on ProfileRecordTypeVisibility and layout assignment lives on Profile.layoutAssignments, neither of which has a permission set equivalent. A PSG migration that trims profiles therefore deletes the default rather than moving it.
- Deleting a record type does not delete the records — When you delete a record type, existing records are reassigned to the default record type for their owner's profile. This can silently change business process membership and picklist value visibility on thousands of records with no audit trail beyond the record type field history (if enabled).
Output Artifacts
| Artifact | Description |
|---|
| Record type rationalization plan | Document listing each object's current and target record type count, consolidation mapping, and Dynamic Forms eligibility |
| Layout assignment matrix | Before and after grid of record types by profiles showing assignment reductions |
| Migration checklist | Step-by-step checklist for data migration, metadata deployment, and post-deployment validation |
Reference Files
| File | Read it when |
|---|
references/metadata-examples.md | Authoring the assignment matrix, rendering it into Profile and PermissionSet XML, running the consolidation and its Bulk API migration, generating the package.xml members, or writing the per-persona describe audit |
references/gotchas.md | Nine platform behaviours behind record type incidents at scale — the vanished default after a PSG migration, the deleted fallback layout row, the save order a bulk RecordTypeId update actually runs, the mass profile diff after deactivation, and the wildcard that returns nothing |
references/examples.md | Sizing a real consolidation: the Opportunity layout-explosion worked example, the hardcoded-Id failure, and the evidence you need before merging record types that only differ by label |
references/well-architected.md | Justifying the design against the pillars, the granularity-vs-maintainability tradeoff, or tracing any claim in this package back to its official source |
references/llm-anti-patterns.md | Reviewing AI-generated record type guidance before acting on it |
templates/record-type-strategy-at-scale-template.md | Capturing the current state, target state, and migration plan before any XML is written |
scripts/check_record_type_strategy_at_scale.py | Linting a retrieved tree: orphan record types, profiles with no default, record types with no layout assignment, count thresholds, hardcoded Ids |
Related Skills
- admin/record-types-and-page-layouts: The single-record-type shapes this skill builds on —
RecordType, BusinessProcess, and Layout XML, the four-object businessProcess rule, the count guide, and the eleven core gotchas. Read it first if the question is "how do I set one up", not "how do I govern ninety".
- admin/record-type-id-management: Resolving a record type Id in Apex, Flow, formulas, and test data. This skill only states the portability rule; that one has the patterns.
- admin/permission-set-architecture: Designing the permission sets and PSGs whose
recordTypeVisibilities fill the visible column of the matrix.
- admin/permission-sets-vs-profiles: The wider set of settings that are profile-only, of which the record type default and layout assignment are two. Read before any PSG migration that touches objects with record types.
- admin/dynamic-forms-and-actions: Building the FlexiPage and its component visibility filters once consolidation has decided a record type is not needed.
- admin/dynamic-forms-migration: Converting one page layout to Dynamic Forms, including current object support.
- admin/picklist-and-value-sets: Global value sets and the picklist model underneath the record type override matrix — read before unioning values for a merge.