Build a Record Type Router (Apex + Metadata)

This is the step-by-step build for Don't Add a Second Door. If you haven't read that yet, start there, it covers the why. Come back here for the how: the Apex, the metadata, and the Aura component to wire it all together.

Design Overview

Routing policy lives in metadata, not code. Add a guided record type by adding a row, remove it by deactivating one. A missing rule defaults to the standard create page, nothing breaks.

The Aura component reads the object from the page reference, so one component covers any object's New button. This guide builds and tests on Cases, but it's not Case-specific.

flowchart LR
    A([New button]) --> B[Aura]
    B --> C[Apex router]
    C --> D{Metadata}
    D -- Guided --> E([Flow])
    D -- No rule --> F([Standard])

Metadata Model

Create Custom Metadata Type

SF_record-type-config-mdt-setup.png

Create Metadata Fields

Label Field Name (enter this) Type Length Description Field Manageability
Object API Name Object_Api_Name Text 80 The object this row applies to (for example Case). Leave default
Record Type Developer Name Record_Type_Developer_Name Text 80 The RecordType.DeveloperName this row applies to. Leave default
Guided Flow API Name Guided_Flow_Api_Name Text 255 Flow API name to launch when this route is guided. Leave default
Is Active Is_Active Checkbox n/a Include this row in routing only when checked. Leave default

SF_record-type-config-mdt-fields.png

We'll come back to populate this once everything below is in place.

Create Apex Class

public with sharing class RecordTypeNewRouter {

    public static final String MODE_STANDARD = 'STANDARD';
    public static final String MODE_GUIDED = 'GUIDED';

    public class RouteRequest {
        @AuraEnabled public String objectApiName { get; set; }
        @AuraEnabled public String recordTypeId { get; set; }
        @AuraEnabled public String contextRecordId { get; set; }
        @AuraEnabled public String contextObjectApiName { get; set; }
    }

    public class RouteResponse {
        @AuraEnabled public String mode { get; set; }
        @AuraEnabled public String guidedFlowApiName { get; set; }
        @AuraEnabled public String outputRecordTypeId { get; set; }
        @AuraEnabled public String contextRecordId { get; set; }
        @AuraEnabled public String contextObjectApiName { get; set; }
        @AuraEnabled public String defaultFieldValues { get; set; }
    }

    public class RouteConfig {
        public String recordTypeDeveloperName;
        public String guidedFlowApiName;
    }

    public interface RouteConfigProvider {
        List<RouteConfig> getActiveConfigs(String objectApiName);
    }

    public interface RecordTypeResolver {
        String getDeveloperName(String objectApiName, Id recordTypeId);
    }

    private class CmdtRouteConfigProvider implements RouteConfigProvider {
        public List<RouteConfig> getActiveConfigs(String objectApiName) {
            List<RouteConfig> configs = new List<RouteConfig>();
            for (Record_Type_Config__mdt row : [
                SELECT Record_Type_Developer_Name__c, Guided_Flow_Api_Name__c
                FROM Record_Type_Config__mdt
                WHERE Object_Api_Name__c = :objectApiName
                  AND Is_Active__c = true
            ]) {
                RouteConfig config = new RouteConfig();
                config.recordTypeDeveloperName = row.Record_Type_Developer_Name__c;
                config.guidedFlowApiName = row.Guided_Flow_Api_Name__c;
                configs.add(config);
            }
            return configs;
        }
    }

    private class SoqlRecordTypeResolver implements RecordTypeResolver {
        public String getDeveloperName(String objectApiName, Id recordTypeId) {
            if (String.isBlank(objectApiName) || recordTypeId == null) {
                return null;
            }
            List<RecordType> rows = [
                SELECT DeveloperName
                FROM RecordType
                WHERE Id = :recordTypeId
                  AND SObjectType = :objectApiName
                LIMIT 1
            ];
            return rows.isEmpty() ? null : rows[0].DeveloperName;
        }
    }

    @TestVisible private static RouteConfigProvider routeConfigProvider = new CmdtRouteConfigProvider();
    @TestVisible private static RecordTypeResolver recordTypeResolver = new SoqlRecordTypeResolver();

    @TestVisible
    private static void setProviders(RouteConfigProvider configProvider, RecordTypeResolver resolver) {
        routeConfigProvider = (configProvider == null) ? new CmdtRouteConfigProvider() : configProvider;
        recordTypeResolver = (resolver == null) ? new SoqlRecordTypeResolver() : resolver;
    }

    @AuraEnabled(cacheable=true)
    public static RouteResponse route(RouteRequest request) {
        RouteResponse response = new RouteResponse();
        response.mode = MODE_STANDARD;

        if (request == null || String.isBlank(request.objectApiName)) {
            return response;
        }

        String objectApiName = request.objectApiName.trim();
        Id recordTypeId = parseId(request.recordTypeId);
        String contextRecordId = String.isBlank(request.contextRecordId) ? null : request.contextRecordId.trim();
        String contextObjectApiName = String.isBlank(request.contextObjectApiName) ? null : request.contextObjectApiName.trim();

        response.outputRecordTypeId = (recordTypeId == null) ? null : String.valueOf(recordTypeId);
        response.contextRecordId = contextRecordId;
        response.contextObjectApiName = contextObjectApiName;

        String recordTypeDeveloperName = recordTypeResolver.getDeveloperName(objectApiName, recordTypeId);
        if (!String.isBlank(recordTypeDeveloperName)) {
            Map<String, RouteConfig> byRecordTypeDeveloperName = new Map<String, RouteConfig>();
            for (RouteConfig cfg : routeConfigProvider.getActiveConfigs(objectApiName)) {
                if (cfg == null || String.isBlank(cfg.recordTypeDeveloperName)) {
                    continue;
                }
                byRecordTypeDeveloperName.put(cfg.recordTypeDeveloperName, cfg);
            }

            RouteConfig match = byRecordTypeDeveloperName.get(recordTypeDeveloperName);
            if (match != null && !String.isBlank(match.guidedFlowApiName)) {
                response.mode = MODE_GUIDED;
                response.guidedFlowApiName = match.guidedFlowApiName.trim();
            }
        }

        response.defaultFieldValues = buildDefaultFieldValues(objectApiName, contextRecordId, contextObjectApiName);

        return response;
    }

    @TestVisible
    private static String buildDefaultFieldValues(String objectApiName, String contextRecordId, String contextObjectApiName) {
        if (String.isBlank(contextRecordId)) {
            return null;
        }

        List<String> pairs = new List<String>();

        // Field prefill is currently Case-specific. Extend this method for
        // other objects if needed.

        // contextObjectApiName is the reliable check. The startsWith prefix
        // is a defensive fallback for when decodeContext fails silently
        // because Salesforce changes their internal page reference format.

        Boolean isAccountContext = contextObjectApiName == 'Account' || contextRecordId.startsWith('001');
        Boolean isContactContext = contextObjectApiName == 'Contact' || contextRecordId.startsWith('003');

        if (objectApiName == 'Case' && isAccountContext) {
            pairs.add('AccountId=' + contextRecordId);
        }

        if (objectApiName == 'Case' && isContactContext) {
            pairs.add('ContactId=' + contextRecordId);
            // Also prefill AccountId from the Contact's parent Account
            String accountId = getAccountIdFromContact(contextRecordId);
            if (accountId != null) {
                pairs.add('AccountId=' + accountId);
            }
        }

        return pairs.isEmpty() ? null : String.join(pairs, ',');
    }

    private static String getAccountIdFromContact(String contactId) {
        try {
            List<Contact> contacts = [
                SELECT AccountId FROM Contact WHERE Id = :contactId LIMIT 1
            ];
            return (contacts.isEmpty() || contacts[0].AccountId == null)
                ? null
                : String.valueOf(contacts[0].AccountId);
        } catch (Exception ex) {
            System.debug(LoggingLevel.WARN, 'RecordTypeNewRouter: failed to resolve AccountId from Contact ' + contactId + ': ' + ex.getMessage());
            return null;
        }
    }

    private static Id parseId(String rawId) {
        if (String.isBlank(rawId)) {
            return null;
        }
        String trimmed = rawId.trim();
        try {
            return (Id) trimmed;
        } catch (Exception ex) {
            return null;
        }
    }
}

Create Apex Test Class

75% coverage to deploy. Don't skip it.

@IsTest
private class RecordTypeNewRouterTest {

    private class FakeConfigProvider implements RecordTypeNewRouter.RouteConfigProvider {
        private List<RecordTypeNewRouter.RouteConfig> configs;
        FakeConfigProvider(List<RecordTypeNewRouter.RouteConfig> configs) {
            this.configs = configs;
        }
        public List<RecordTypeNewRouter.RouteConfig> getActiveConfigs(String objectApiName) {
            return configs;
        }
    }

    private class FakeRecordTypeResolver implements RecordTypeNewRouter.RecordTypeResolver {
        private String devName;
        FakeRecordTypeResolver(String devName) {
            this.devName = devName;
        }
        public String getDeveloperName(String objectApiName, Id recordTypeId) {
            return devName;
        }
    }

    private static RecordTypeNewRouter.RouteConfig guidedConfig(String devName, String flowName) {
        RecordTypeNewRouter.RouteConfig cfg = new RecordTypeNewRouter.RouteConfig();
        cfg.recordTypeDeveloperName = devName;
        cfg.guidedFlowApiName = flowName;
        return cfg;
    }

    // --- Routing tests ---

    @IsTest
    static void routesGuidedWhenAllowlisted() {
        RecordTypeNewRouter.setProviders(
            new FakeConfigProvider(new List<RecordTypeNewRouter.RouteConfig>{
                guidedConfig('Technical_Support', 'Case_Create_Technical_Support')
            }),
            new FakeRecordTypeResolver('Technical_Support')
        );

        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = 'Case';
        req.recordTypeId = '012000000000000AAA';

        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);

        System.assertEquals(RecordTypeNewRouter.MODE_GUIDED, res.mode);
        System.assertEquals('Case_Create_Technical_Support', res.guidedFlowApiName);
    }

    @IsTest
    static void routesStandardWhenNoMetadataRule() {
        RecordTypeNewRouter.setProviders(
            new FakeConfigProvider(new List<RecordTypeNewRouter.RouteConfig>()),
            new FakeRecordTypeResolver('Billing')
        );

        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = 'Case';
        req.recordTypeId = '012000000000000AAA';

        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);

        System.assertEquals(RecordTypeNewRouter.MODE_STANDARD, res.mode);
        System.assertEquals(null, res.guidedFlowApiName);
    }

    @IsTest
    static void routesStandardWhenRecordTypeNotFound() {
        // Covers the path where the resolver returns null (valid ID format
        // but no matching RecordType in the org).
        RecordTypeNewRouter.setProviders(
            new FakeConfigProvider(new List<RecordTypeNewRouter.RouteConfig>{
                guidedConfig('Technical_Support', 'Case_Create_Technical_Support')
            }),
            new FakeRecordTypeResolver(null)
        );

        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = 'Case';
        req.recordTypeId = '012000000000000AAA';

        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);

        System.assertEquals(RecordTypeNewRouter.MODE_STANDARD, res.mode);
    }

    @IsTest
    static void routesStandardWhenFlowNameBlank() {
        RecordTypeNewRouter.setProviders(
            new FakeConfigProvider(new List<RecordTypeNewRouter.RouteConfig>{
                guidedConfig('Refunds', '')
            }),
            new FakeRecordTypeResolver('Refunds')
        );

        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = 'Case';
        req.recordTypeId = '012000000000000AAA';

        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);

        System.assertEquals(RecordTypeNewRouter.MODE_STANDARD, res.mode);
    }

    @IsTest
    static void routesStandardWhenRequestNull() {
        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(null);

        System.assertEquals(RecordTypeNewRouter.MODE_STANDARD, res.mode);
        System.assertEquals(null, res.defaultFieldValues);
    }

    @IsTest
    static void routesStandardWhenObjectApiNameBlank() {
        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = '';

        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);

        System.assertEquals(RecordTypeNewRouter.MODE_STANDARD, res.mode);
    }

    // --- Default field value tests ---

    @IsTest
    static void prefillsAccountIdFromAccountContext() {
        RecordTypeNewRouter.setProviders(
            new FakeConfigProvider(new List<RecordTypeNewRouter.RouteConfig>()),
            new FakeRecordTypeResolver('Billing')
        );

        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = 'Case';
        req.recordTypeId = '012000000000000AAA';
        req.contextRecordId = '001000000000000AAA';
        req.contextObjectApiName = 'Account';

        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);

        System.assertEquals('AccountId=001000000000000AAA', res.defaultFieldValues);
    }

    @IsTest
    static void prefillsAccountIdByPrefixFallback() {
        // Prefix fallback covers cases where decodeContext fails to extract
        // contextObjectApiName but still has the record ID.
        RecordTypeNewRouter.setProviders(
            new FakeConfigProvider(new List<RecordTypeNewRouter.RouteConfig>()),
            new FakeRecordTypeResolver('Billing')
        );

        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = 'Case';
        req.recordTypeId = '012000000000000AAA';
        req.contextRecordId = '001000000000000AAA';
        req.contextObjectApiName = null;

        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);

        System.assert(res.defaultFieldValues != null && res.defaultFieldValues.contains('AccountId=001000000000000AAA'));
    }

    // This test uses Test.startTest()/stopTest() because it inserts real
    // records and hits real SOQL (getAccountIdFromContact). The other tests
    // use fakes and don't need it.
    @IsTest
    static void prefillsContactIdFromContactContext() {
        RecordTypeNewRouter.setProviders(
            new FakeConfigProvider(new List<RecordTypeNewRouter.RouteConfig>()),
            new FakeRecordTypeResolver('Billing')
        );

        Account testAccount = new Account(Name = 'Test Account');
        insert testAccount;
        Contact testContact = new Contact(
            FirstName = 'Test', LastName = 'Contact', AccountId = testAccount.Id
        );
        insert testContact;

        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = 'Case';
        req.recordTypeId = '012000000000000AAA';
        req.contextRecordId = String.valueOf(testContact.Id);
        req.contextObjectApiName = 'Contact';

        Test.startTest();
        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);
        Test.stopTest();

        System.assert(res.defaultFieldValues.contains('ContactId=' + testContact.Id),
            'Should prefill ContactId');
        System.assert(res.defaultFieldValues.contains('AccountId=' + testAccount.Id),
            'Should also prefill AccountId from the Contact parent');
    }

    @IsTest
    static void noDefaultFieldValuesWithoutContext() {
        RecordTypeNewRouter.setProviders(
            new FakeConfigProvider(new List<RecordTypeNewRouter.RouteConfig>()),
            new FakeRecordTypeResolver('Billing')
        );

        RecordTypeNewRouter.RouteRequest req = new RecordTypeNewRouter.RouteRequest();
        req.objectApiName = 'Case';
        req.recordTypeId = '012000000000000AAA';

        RecordTypeNewRouter.RouteResponse res = RecordTypeNewRouter.route(req);

        System.assertEquals(null, res.defaultFieldValues);
    }
}

Create Aura Component Bundle

One component covers any object's New button. It reads objectApiName from the page reference, so no object-specific code is ever needed.

RecordTypeNewOverride.cmp

<aura:component controller="RecordTypeNewRouter" implements="lightning:actionOverride,lightning:hasPageReference">
    <aura:attribute name="pageReference" type="Object" />
    <aura:attribute name="isRouting" type="Boolean" default="false" />
    <aura:attribute name="objectApiName" type="String" default="" />
    <aura:attribute name="backgroundContext" type="String" default="" />
    <aura:attribute name="navigationLocation" type="String" default="" />
    <aura:handler name="init" value="{!this}" action="{!c.start}" />
    <aura:handler name="change" value="{!v.pageReference}" action="{!c.start}" />
    <lightning:navigation aura:id="navService" />
    <lightning:flow aura:id="guidedFlow" onstatuschange="{!c.handleFlowStatus}" />
</aura:component>

RecordTypeNewOverrideController.js

({
    start : function(component, event, helper) {
        var pageReference = component.get("v.pageReference");
        // pageReference may not be populated during init; the change
        // handler will re-invoke start once Salesforce provides it.
        if (!pageReference || !pageReference.state) {
            return;
        }
        // Guard against init/change double-fire while routing is in flight.
        // Do not persist dedupe across launches, or valid later launches
        // can be skipped and leave a blank override shell.
        if (component.get("v.isRouting")) {
            return;
        }
        component.set("v.isRouting", true);

        var state = pageReference.state;

        var objectApiName = (pageReference.attributes && pageReference.attributes.objectApiName) || "";
        component.set("v.objectApiName", objectApiName);

        var recordTypeId = state.recordTypeId || "";
        var context = helper.decodeContext(state.inContextOfRef);
        var fallbackFilter = state.filterName || "Recent";
        var backgroundContext = state.backgroundContext || context.backgroundContext
            || "/lightning/o/" + objectApiName + "/list?filterName=" + encodeURIComponent(fallbackFilter);

        component.set("v.backgroundContext", backgroundContext);
        component.set("v.navigationLocation", state.navigationLocation || "");

        if (!recordTypeId) {
            helper.navigateStandard(component, {
                outputRecordTypeId: null,
                defaultFieldValues: null
            });
            return;
        }

        var action = component.get("c.route");
        action.setParams({
            request: {
                objectApiName: objectApiName,
                recordTypeId: recordTypeId,
                contextRecordId: context.recordId,
                contextObjectApiName: context.objectApiName
            }
        });
        action.setCallback(this, function(resp) {
            var respState = resp.getState();
            if (respState !== "SUCCESS") {
                if (respState === "ERROR") {
                    console.error("RecordTypeNewRouter.route error", resp.getError());
                }
                helper.navigateStandard(component, {
                    outputRecordTypeId: recordTypeId,
                    defaultFieldValues: null
                });
                return;
            }

            var route = resp.getReturnValue();
            if (route && route.mode === "GUIDED" && route.guidedFlowApiName) {
                try {
                    component.find("guidedFlow").startFlow(route.guidedFlowApiName, [
                        { name : "contextRecordId", type : "String", value : route.contextRecordId || "" },
                        { name : "contextObjectApiName", type : "String", value : route.contextObjectApiName || "" }
                    ]);
                    component.set("v.isRouting", false);
                } catch (e) {
                    console.error("Guided flow launch failed; falling back to standard create", e);
                    helper.navigateStandard(component, {
                        outputRecordTypeId: (route.outputRecordTypeId || recordTypeId),
                        defaultFieldValues: route.defaultFieldValues || null
                    });
                }
            } else {
                route = route || {};
                if (!route.outputRecordTypeId) {
                    route.outputRecordTypeId = recordTypeId;
                }
                helper.navigateStandard(component, route);
            }
        });
        $A.enqueueAction(action);
    },

    // backgroundContext holds the originating record or list view URL.
    // ERROR status fires when the flow is not found or has no active version.
    handleFlowStatus : function(component, event, helper) {
        var status = event.getParam("status");
        if (status === "ERROR") {
            console.error("Guided flow error; falling back to standard create", event.getParam("error"));
            helper.navigateStandard(component, {});
            return;
        }
        if (status === "FINISHED" || status === "FINISHED_SCREEN") {
            // If the flow handled navigation itself (for example via an LWC
            // local action), do not also navigate from the host override.
            // Requires flow variable navigateToRecordId to be Available for output.
            var outputs = event.getParam("outputVariables") || [];
            var navigateToRecordId = "";
            outputs.forEach(function(o) {
                if (o.name === "navigateToRecordId") {
                    navigateToRecordId = o.value || "";
                }
            });
            if (navigateToRecordId) {
                return;
            }

            var backgroundContext = component.get("v.backgroundContext");
            if (backgroundContext) {
                var navEvent = $A.get("e.force:navigateToURL");
                navEvent.setParams({ url : backgroundContext });
                navEvent.fire();
            } else {
                var objectApiName = component.get("v.objectApiName");
                navEvent = $A.get("e.force:navigateToObjectHome");
                navEvent.setParams({ scope : objectApiName });
                navEvent.fire();
            }
        }
    }
})

RecordTypeNewOverrideHelper.js

({
    // nooverride: "1" tells Salesforce to skip the action override
    // (this component) and show the native create page instead.
    navigateStandard : function(component, route) {
        component.set("v.isRouting", false);
        var objectApiName = component.get("v.objectApiName");
        var navState = { nooverride : "1" };
        var navigationLocation = component.get("v.navigationLocation");
        var backgroundContext = component.get("v.backgroundContext");
        if (navigationLocation) {
            navState.navigationLocation = navigationLocation;
        }
        if (backgroundContext) {
            navState.backgroundContext = backgroundContext;
        }
        if (route && route.outputRecordTypeId) {
            navState.recordTypeId = route.outputRecordTypeId;
        }
        if (route && route.defaultFieldValues) {
            navState.defaultFieldValues = route.defaultFieldValues;
        }
        component.find("navService").navigate({
            type : "standard__objectPage",
            attributes : { objectApiName : objectApiName, actionName : "new" },
            state : navState
        }, true);
    },

    // Decode the parent record context from inContextOfRef.
    // Salesforce encodes the originating page reference as a base64 string
    // with URL-safe characters (- instead of +, _ instead of /).
    // This is an undocumented internal format; see Caveats section.
    decodeContext : function(inContextOfRef) {
        var result = { recordId: "", objectApiName: "", backgroundContext: "" };
        if (!inContextOfRef) {
            return result;
        }
        try {
            var encoded = inContextOfRef;
            if (encoded.indexOf(".") !== -1) {
                encoded = encoded.substring(encoded.indexOf(".") + 1);
            }
            encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
            while (encoded.length % 4) {
                encoded += "=";
            }
            var ref = JSON.parse(window.atob(encoded));
            if (ref.attributes) {
                result.recordId = ref.attributes.recordId || "";
                result.objectApiName = ref.attributes.objectApiName || "";
                if (ref.attributes.recordId && ref.attributes.objectApiName) {
                    result.backgroundContext = "/lightning/r/" + ref.attributes.objectApiName + "/" + ref.attributes.recordId + "/view";
                } else if (ref.attributes.objectApiName) {
                    var filterName = (ref.state && ref.state.filterName) ? ref.state.filterName : "Recent";
                    result.backgroundContext = "/lightning/o/" + ref.attributes.objectApiName + "/list?filterName=" + encodeURIComponent(filterName);
                }
            }
            return result;
        } catch (e) {
            return result;
        }
    }
})

Override the New Button

Setup -> Object Manager -> Case -> Buttons, Links, and Actions -> New -> Edit.

SF_case-new-button-override.png

With no metadata rows in place yet, every record type routes to standard create. Test each one and confirm behavior looks identical to before the override was applied. That's the point. Non-guided record types just work, no configuration needed.

Create a Test Flow

A minimal flow to verify end-to-end routing. The example below creates a Case with optional account context and navigates to the new record. Your guided flow logic will differ.

Create three variables:

API Name Data Type Available for Input Available for Output Description
contextRecordId Text checked unchecked Parent record ID passed in by the Aura component.
contextObjectApiName Text checked unchecked API name of the context object (for example Account).
navigateToRecordId Text unchecked checked ID of the record to open. Aura reads this from outputVariables to avoid double navigation.
  1. Add a Create Records element and create a Case. The exact field mapping will vary by use case.
  2. Add an Assignment element. Set navigateToRecordId to the created Case ID.
  3. Add an Action element and select flowRecordNavigator (set up in Navigate to a Record from a Screen Flow). Map navigateToRecordId to {!navigateToRecordId}.

flow-test-guided-flow-overview-example.png

Add Your Records

Only add rows for guided record types. No row means standard behavior.

Populate this table once the rest of the setup is complete and tested.

[!todo] Add screenshot of the Custom Metadata records list after adding these rows.

Object RecordTypeDeveloperName GuidedFlowApiName Active
Case Technical_Support Case_Create_Technical_Support true
Case Refunds Case_Create_Refunds true

Notes

The Aura component is Aura because it has to be. Salesforce doesn't support LWC for standard action overrides. If that changes, migrate it.

The Apex route method uses @AuraEnabled(cacheable=true), so routing is fast but metadata changes may not show immediately. Hard-refresh (Ctrl+Shift+R) clears it. Drop cacheable=true if you need immediate consistency.

Adding an action override changes where Salesforce takes you after saving, even on the standard path.

New from list view New from related list
No override Opens new record Stays on parent, green toast
This override (standard path) Opens new record Opens new record
This override (guided path) TBC TBC