diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 4e05bed5714c..c3833805240c 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -887,6 +887,7 @@ const CONST = {
         BULK_DUPLICATE_REPORT: 'bulkDuplicateReport',
         BULK_EDIT: 'bulkEdit',
         NEW_MANUAL_EXPENSE_FLOW: 'newManualExpenseFlow',
+        SUBMIT_2026: 'submit2026',
         BULK_SUBMIT_APPROVE_PAY: 'bulkSubmitApprovePay',
     },
     BUTTON_STATES: {
@@ -3515,6 +3516,8 @@ const CONST = {
 
             // Often referred to as "collect" workspaces
             TEAM: 'team',
+
+            SUBMIT: 'submit2026',
         },
         RULE_CONDITIONS: {
             MATCHES: 'matches',
@@ -3533,6 +3536,7 @@ const CONST = {
             ADMIN: 'admin',
             AUDITOR: 'auditor',
             USER: 'user',
+            EDITOR: 'editor',
         },
         AUTO_REIMBURSEMENT_MAX_LIMIT_CENTS: 2000000,
 
diff --git a/src/components/SidePanel/SidePanelContextProvider.tsx b/src/components/SidePanel/SidePanelContextProvider.tsx
index 87fb63d098a4..c1dee29c6d69 100644
--- a/src/components/SidePanel/SidePanelContextProvider.tsx
+++ b/src/components/SidePanel/SidePanelContextProvider.tsx
@@ -10,7 +10,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
 import SidePanelActions from '@libs/actions/SidePanel';
 import DateUtils from '@libs/DateUtils';
 import focusComposerWithDelay from '@libs/focusComposerWithDelay';
-import {isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, shouldShowPolicy} from '@libs/PolicyUtils';
 import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
 import variables from '@styles/variables';
 import CONST from '@src/CONST';
@@ -82,7 +82,7 @@ function SidePanelContextProvider({children}: PropsWithChildren) {
 
     const isRHPAdminsRoom = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM;
     const isRHPHomePage = onboardingRHPVariant === CONST.ONBOARDING_RHP_VARIANT.RHP_HOME_PAGE;
-    const isUserAdmin = isPolicyAdmin(activePolicy, sessionEmail);
+    const isUserAdmin = canEditWorkspaceSettings(activePolicy);
     const isPolicyActive = shouldShowPolicy(activePolicy, false, sessionEmail ?? '');
     const adminsChatReportID = activePolicy?.chatReportIDAdmins?.toString();
 
diff --git a/src/components/WorkspaceMemberRoleList.tsx b/src/components/WorkspaceMemberRoleList.tsx
index 1263e0d38d8c..e6f4b8097854 100644
--- a/src/components/WorkspaceMemberRoleList.tsx
+++ b/src/components/WorkspaceMemberRoleList.tsx
@@ -1,12 +1,15 @@
+import {emailSelector} from '@selectors/Session';
 import React from 'react';
 import {View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
 import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
 import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
-import {isControlPolicy} from '@libs/PolicyUtils';
+import {isControlPolicy, isPolicyAdmin} from '@libs/PolicyUtils';
 import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
 import type {Route} from '@src/ROUTES';
 import type {Policy} from '@src/types/onyx';
 import HeaderWithBackButton from './HeaderWithBackButton';
@@ -32,6 +35,7 @@ type WorkspaceMemberRoleListProps = {
 function WorkspaceMemberRoleList({role, policy, navigateBackTo = undefined, isLoading = false, onSelectRole = () => {}}: WorkspaceMemberRoleListProps) {
     const {translate} = useLocalize();
     const styles = useThemeStyles();
+    const [currentUserEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
 
     const workspaceRoles: ListItemType[] = [
         {
@@ -58,7 +62,18 @@ function WorkspaceMemberRoleList({role, policy, navigateBackTo = undefined, isLo
     ];
 
     const isPolicyControl = isControlPolicy(policy);
-    const availableRoleItems: ListItemType[] = workspaceRoles.filter((item) => isPolicyControl || item.value !== CONST.POLICY.ROLE.AUDITOR);
+    // Only strict admins can assign the ADMIN role. Editors (e.g. Submit workspace owners) can
+    // invite/manage members but must not be able to escalate anyone to admin.
+    const canAssignAdminRole = isPolicyAdmin(policy, currentUserEmail);
+    const availableRoleItems: ListItemType[] = workspaceRoles.filter((item) => {
+        if (item.value === CONST.POLICY.ROLE.AUDITOR && !isPolicyControl) {
+            return false;
+        }
+        if (item.value === CONST.POLICY.ROLE.ADMIN && !canAssignAdminRole) {
+            return false;
+        }
+        return true;
+    });
 
     return (
         <>
diff --git a/src/hooks/useAutoCreateSubmitWorkspace.ts b/src/hooks/useAutoCreateSubmitWorkspace.ts
new file mode 100644
index 000000000000..7d97960bffcb
--- /dev/null
+++ b/src/hooks/useAutoCreateSubmitWorkspace.ts
@@ -0,0 +1,115 @@
+import {useCallback, useMemo} from 'react';
+import type {OnyxCollection} from 'react-native-onyx';
+import {navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding';
+import {createDisplayName} from '@libs/PersonalDetailsUtils';
+import {canEditWorkspaceSettings, isGroupPolicy} from '@libs/PolicyUtils';
+import {createWorkspace, generateDefaultWorkspaceName, generatePolicyID} from '@userActions/Policy/Policy';
+import {completeOnboarding} from '@userActions/Report';
+import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActions/Welcome';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Policy} from '@src/types/onyx';
+import useOnboardingWorkspaceCreationState from './useOnboardingWorkspaceCreationState';
+import useOnyx from './useOnyx';
+
+/**
+ * Hook that provides a function to auto-create a Submit workspace for EMPLOYER
+ * users during onboarding and complete the onboarding flow.
+ *
+ * Shared by BaseOnboardingPersonalDetails, BaseOnboardingPurpose, and BaseOnboardingWorkspaces.
+ */
+function useAutoCreateSubmitWorkspace() {
+    const {
+        onboardingPolicyID,
+        onboardingAdminsChatReportID,
+        introSelected,
+        isSelfTourViewed,
+        betas,
+        currentUserEmail,
+        currentUserAccountID,
+        localCurrencyCode,
+        activePolicy,
+        translate,
+        formatPhoneNumber,
+        isRestrictedPolicyCreation,
+        hasActiveAdminPolicies,
+        onboardingMessages,
+        lastWorkspaceNumber,
+        isSmallScreenWidth,
+    } = useOnboardingWorkspaceCreationState();
+
+    const groupPolicySelector = useMemo(
+        () => (policies: OnyxCollection<Policy>) => Object.values(policies ?? {}).some((policy) => isGroupPolicy(policy) && canEditWorkspaceSettings(policy)),
+        [],
+    );
+    const [hasEditableGroupPolicy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: groupPolicySelector});
+
+    const autoCreateSubmitWorkspace = useCallback(
+        (firstName: string, lastName: string) => {
+            const shouldCreateWorkspace = !isRestrictedPolicyCreation && !onboardingPolicyID && !hasEditableGroupPolicy;
+            const displayName = createDisplayName(currentUserEmail, {firstName, lastName}, formatPhoneNumber);
+
+            const {adminsChatReportID: newAdminsChatReportID, policyID: newPolicyID} = shouldCreateWorkspace
+                ? createWorkspace({
+                      policyOwnerEmail: undefined,
+                      makeMeAdmin: true,
+                      policyName: generateDefaultWorkspaceName(currentUserEmail, lastWorkspaceNumber, translate, displayName),
+                      policyID: generatePolicyID(),
+                      engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                      currency: localCurrencyCode,
+                      file: undefined,
+                      shouldAddOnboardingTasks: false,
+                      introSelected,
+                      activePolicy,
+                      currentUserAccountIDParam: currentUserAccountID,
+                      currentUserEmailParam: currentUserEmail,
+                      shouldAddGuideWelcomeMessage: false,
+                      type: CONST.POLICY.TYPE.SUBMIT,
+                      betas,
+                      isSelfTourViewed,
+                      hasActiveAdminPolicies,
+                  })
+                : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID};
+
+            completeOnboarding({
+                engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                onboardingMessage: onboardingMessages[CONST.ONBOARDING_CHOICES.EMPLOYER],
+                firstName,
+                lastName,
+                adminsChatReportID: newAdminsChatReportID,
+                onboardingPolicyID: newPolicyID,
+                introSelected,
+                isSelfTourViewed,
+                betas,
+            });
+
+            setOnboardingAdminsChatReportID();
+            setOnboardingPolicyID();
+
+            navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(newPolicyID, isSmallScreenWidth);
+        },
+        [
+            currentUserEmail,
+            currentUserAccountID,
+            lastWorkspaceNumber,
+            translate,
+            formatPhoneNumber,
+            isRestrictedPolicyCreation,
+            onboardingPolicyID,
+            hasEditableGroupPolicy,
+            onboardingAdminsChatReportID,
+            localCurrencyCode,
+            introSelected,
+            activePolicy,
+            isSelfTourViewed,
+            onboardingMessages,
+            betas,
+            hasActiveAdminPolicies,
+            isSmallScreenWidth,
+        ],
+    );
+
+    return autoCreateSubmitWorkspace;
+}
+
+export default useAutoCreateSubmitWorkspace;
diff --git a/src/hooks/useAutoCreateTrackWorkspace.ts b/src/hooks/useAutoCreateTrackWorkspace.ts
index 78dc14f3edb9..99cf11eef483 100644
--- a/src/hooks/useAutoCreateTrackWorkspace.ts
+++ b/src/hooks/useAutoCreateTrackWorkspace.ts
@@ -1,4 +1,3 @@
-import {hasSeenTourSelector} from '@selectors/Onboarding';
 import {useCallback, useMemo} from 'react';
 import type {OnyxCollection} from 'react-native-onyx';
 import isSidePanelReportSupported from '@components/SidePanel/isSidePanelReportSupported';
@@ -12,17 +11,10 @@ import {setOnboardingAdminsChatReportID, setOnboardingPolicyID} from '@userActio
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {OnboardingPurpose, OnboardingRHPVariant, Policy} from '@src/types/onyx';
-import useActivePolicy from './useActivePolicy';
 import useArchivedReportsIdSet from './useArchivedReportsIdSet';
-import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
-import useHasActiveAdminPolicies from './useHasActiveAdminPolicies';
-import useLastWorkspaceNumber from './useLastWorkspaceNumber';
-import useLocalize from './useLocalize';
-import useOnboardingMessages from './useOnboardingMessages';
+import useOnboardingWorkspaceCreationState from './useOnboardingWorkspaceCreationState';
 import useOnyx from './useOnyx';
 import usePermissions from './usePermissions';
-import usePreferredPolicy from './usePreferredPolicy';
-import useResponsiveLayout from './useResponsiveLayout';
 
 /**
  * Hook that provides a function to auto-create a workspace for Track (PERSONAL_SPEND)
@@ -31,57 +23,57 @@ import useResponsiveLayout from './useResponsiveLayout';
  * Shared by BaseOnboardingPersonalDetails and BaseOnboardingPurpose.
  */
 function useAutoCreateTrackWorkspace() {
-    const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID);
-    const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID);
-    const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
-    const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
-    const [betas] = useOnyx(ONYXKEYS.BETAS);
-    const [session] = useOnyx(ONYXKEYS.SESSION);
+    const {
+        onboardingPolicyID,
+        onboardingAdminsChatReportID,
+        introSelected,
+        isSelfTourViewed,
+        betas,
+        currentUserEmail,
+        currentUserAccountID,
+        localCurrencyCode,
+        activePolicy,
+        translate,
+        formatPhoneNumber,
+        isRestrictedPolicyCreation,
+        hasActiveAdminPolicies,
+        onboardingMessages,
+        lastWorkspaceNumber,
+        isSmallScreenWidth,
+    } = useOnboardingWorkspaceCreationState();
+
     const paidGroupPolicySelector = useMemo(
-        () => (policies: OnyxCollection<Policy>) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, session?.email)),
-        [session?.email],
+        () => (policies: OnyxCollection<Policy>) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyAdmin(policy, currentUserEmail)),
+        [currentUserEmail],
     );
     const [hasPaidGroupAdminPolicy] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: paidGroupPolicySelector});
+
     const [conciergeChatReportID = ''] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
     const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING);
-    const currentUserPersonalDetails = useCurrentUserPersonalDetails();
     const archivedReportsIdSet = useArchivedReportsIdSet();
     const {isBetaEnabled} = usePermissions();
-    const {translate, formatPhoneNumber} = useLocalize();
-    const activePolicy = useActivePolicy();
-    const {isRestrictedPolicyCreation} = usePreferredPolicy();
-    const hasActiveAdminPolicies = useHasActiveAdminPolicies();
-    const lastWorkspaceNumber = useLastWorkspaceNumber();
-    const {onboardingMessages} = useOnboardingMessages();
-
-    // We use isSmallScreenWidth instead of shouldUseNarrowLayout because navigateAfterOnboarding
-    // relies on actual device screen width to handle navigation stack differences: on small screens,
-    // removing OnboardingModalNavigator redirects to HOME, requiring explicit navigation to the last
-    // accessed report. This behavior is tied to screen size, not responsive layout mode.
-    // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
-    const {isSmallScreenWidth} = useResponsiveLayout();
 
     const mergedAccountConciergeReportID = !onboardingValues?.shouldRedirectToClassicAfterMerge && onboardingValues?.shouldValidate ? conciergeChatReportID : undefined;
 
     const autoCreateTrackWorkspace = useCallback(
         async (firstName: string, lastName: string, onboardingPurposeSelected: OnboardingPurpose) => {
             const shouldCreateWorkspace = !isRestrictedPolicyCreation && !onboardingPolicyID && !hasPaidGroupAdminPolicy;
-            const displayName = createDisplayName(session?.email ?? '', {firstName, lastName}, formatPhoneNumber);
+            const displayName = createDisplayName(currentUserEmail, {firstName, lastName}, formatPhoneNumber);
 
             const {adminsChatReportID: newAdminsChatReportID, policyID: newPolicyID} = shouldCreateWorkspace
                 ? createWorkspace({
                       policyOwnerEmail: undefined,
                       makeMeAdmin: true,
-                      policyName: generateDefaultWorkspaceName(session?.email ?? '', lastWorkspaceNumber, translate, displayName),
+                      policyName: generateDefaultWorkspaceName(currentUserEmail, lastWorkspaceNumber, translate, displayName),
                       policyID: generatePolicyID(),
                       engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE,
-                      currency: currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD,
+                      currency: localCurrencyCode,
                       file: undefined,
                       shouldAddOnboardingTasks: false,
                       introSelected,
                       activePolicy,
-                      currentUserAccountIDParam: session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
-                      currentUserEmailParam: session?.email ?? '',
+                      currentUserAccountIDParam: currentUserAccountID,
+                      currentUserEmailParam: currentUserEmail,
                       shouldAddGuideWelcomeMessage: false,
                       onboardingPurposeSelected,
                       betas,
@@ -129,8 +121,8 @@ function useAutoCreateTrackWorkspace() {
             }
         },
         [
-            session?.email,
-            session?.accountID,
+            currentUserEmail,
+            currentUserAccountID,
             lastWorkspaceNumber,
             translate,
             formatPhoneNumber,
@@ -138,7 +130,7 @@ function useAutoCreateTrackWorkspace() {
             onboardingPolicyID,
             hasPaidGroupAdminPolicy,
             onboardingAdminsChatReportID,
-            currentUserPersonalDetails.localCurrencyCode,
+            localCurrencyCode,
             introSelected,
             activePolicy,
             isSelfTourViewed,
diff --git a/src/hooks/useOnboardingWorkspaceCreationState.ts b/src/hooks/useOnboardingWorkspaceCreationState.ts
new file mode 100644
index 000000000000..cb09fb0b9a29
--- /dev/null
+++ b/src/hooks/useOnboardingWorkspaceCreationState.ts
@@ -0,0 +1,66 @@
+import {hasSeenTourSelector} from '@selectors/Onboarding';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import useActivePolicy from './useActivePolicy';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import useHasActiveAdminPolicies from './useHasActiveAdminPolicies';
+import useLastWorkspaceNumber from './useLastWorkspaceNumber';
+import useLocalize from './useLocalize';
+import useOnboardingMessages from './useOnboardingMessages';
+import useOnyx from './useOnyx';
+import usePreferredPolicy from './usePreferredPolicy';
+import useResponsiveLayout from './useResponsiveLayout';
+
+/**
+ * Shared state for the onboarding workspace auto-creation hooks
+ * (`useAutoCreateSubmitWorkspace`, `useAutoCreateTrackWorkspace`).
+ *
+ * Email and accountID come from `ONYXKEYS.SESSION` because session is hydrated
+ * earlier in onboarding than personal details.
+ */
+function useOnboardingWorkspaceCreationState() {
+    const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID);
+    const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID);
+    const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
+    const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
+    const [betas] = useOnyx(ONYXKEYS.BETAS);
+    const [session] = useOnyx(ONYXKEYS.SESSION);
+
+    const currentUserEmail = session?.email ?? '';
+    const currentUserAccountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID;
+
+    const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+    const localCurrencyCode = currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD;
+
+    const activePolicy = useActivePolicy();
+    const {translate, formatPhoneNumber} = useLocalize();
+    const {isRestrictedPolicyCreation} = usePreferredPolicy();
+    const hasActiveAdminPolicies = useHasActiveAdminPolicies();
+    const {onboardingMessages} = useOnboardingMessages();
+    const lastWorkspaceNumber = useLastWorkspaceNumber();
+
+    // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+    const {isSmallScreenWidth} = useResponsiveLayout();
+
+    return {
+        onboardingPolicyID,
+        onboardingAdminsChatReportID,
+        introSelected,
+        isSelfTourViewed,
+        betas,
+        currentUserPersonalDetails,
+        currentUserEmail,
+        currentUserAccountID,
+        localCurrencyCode,
+        activePolicy,
+        translate,
+        formatPhoneNumber,
+        isRestrictedPolicyCreation,
+        hasActiveAdminPolicies,
+        onboardingMessages,
+        lastWorkspaceNumber,
+        isSmallScreenWidth,
+    };
+}
+
+export default useOnboardingWorkspaceCreationState;
diff --git a/src/languages/de.ts b/src/languages/de.ts
index c10716e76f13..bf229f1ccc67 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -6989,6 +6989,10 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
                     label: 'Steuerung',
                     description: 'Für Organisationen mit erweiterten Anforderungen.',
                 },
+                submit2026: {
+                    label: 'Einreichen',
+                    description: 'Für Mitarbeiter, die Ausgaben bei ihrem Arbeitgeber einreichen möchten.',
+                },
             },
             description: 'Wähle ein passendes Abo für dich. Eine detaillierte Liste der Funktionen und Preise findest du in unserem',
             subscriptionLink: 'Hilfeseite zu Plantypen und Preisen',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 9f2ab127479b..53ec223f1d75 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -7001,6 +7001,10 @@ const translations = {
                     label: 'Control',
                     description: 'For organizations with advanced requirements.',
                 },
+                submit2026: {
+                    label: 'Submit',
+                    description: 'For employees looking to submit expenses to their employer.',
+                },
             },
             description: "Choose a plan that's right for you. For a detailed list of features and pricing, check out our",
             subscriptionLink: 'plan types and pricing help page',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 0a86328905db..2805891bcb07 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -6417,6 +6417,10 @@ ${amount} para ${merchant} - ${date}`,
                     label: 'Controlar',
                     description: 'Para organizaciones con requisitos avanzados.',
                 },
+                submit2026: {
+                    label: 'Enviar',
+                    description: 'Para empleados que buscan enviar gastos a su empleador.',
+                },
             },
             description: 'Elige el plan adecuado para ti. Para ver una lista detallada de funciones y precios, consulta nuestra',
             subscriptionLink: 'página de ayuda sobre tipos de planes y precios',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index f792e7e5c11b..120dd1b75b2b 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -7011,6 +7011,10 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
                     label: 'Contrôle',
                     description: 'Pour les organisations ayant des exigences avancées.',
                 },
+                submit2026: {
+                    label: 'Soumettre',
+                    description: 'Pour les employés souhaitant soumettre des dépenses à leur employeur.',
+                },
             },
             description: 'Choisissez l’offre qui vous convient. Pour une liste détaillée des fonctionnalités et des tarifs, consultez notre',
             subscriptionLink: "page d'aide sur les types de forfaits et les tarifs",
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 85d165d597be..0cccc4491cdd 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -6974,6 +6974,10 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
                     label: 'Controllo',
                     description: 'Per le organizzazioni con requisiti avanzati.',
                 },
+                submit2026: {
+                    label: 'Invia',
+                    description: 'Per i dipendenti che desiderano inviare le spese al proprio datore di lavoro.',
+                },
             },
             description: 'Scegli il piano più adatto a te. Per un elenco dettagliato di funzionalità e prezzi, consulta la nostra',
             subscriptionLink: 'pagina di aiuto su tipi di piano e prezzi',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index d3f4b3a2163f..2bf9418fd86b 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -6897,6 +6897,10 @@ ${reportName}
                     label: 'コントロール',
                     description: '高度な要件を持つ組織向け。',
                 },
+                submit2026: {
+                    label: '提出',
+                    description: '雇用主に経費を提出したい従業員向け。',
+                },
             },
             description: '自分に合ったプランをお選びください。機能と料金の詳細な一覧は、こちらのページをご覧ください',
             subscriptionLink: 'プランの種類と料金のヘルプページ',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 6a5e6b167ffb..b56b6c3866e3 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -6953,6 +6953,10 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
                     label: 'Beheer',
                     description: 'Voor organisaties met geavanceerde vereisten.',
                 },
+                submit2026: {
+                    label: 'Indienen',
+                    description: 'Voor werknemers die onkosten bij hun werkgever willen indienen.',
+                },
             },
             description: 'Kies een pakket dat bij je past. Voor een gedetailleerd overzicht van functies en prijzen, bekijk onze',
             subscriptionLink: 'hulppagina voor abonnementstypen en prijzen',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index d93360d7bbe1..954f0fdc1ffd 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -6946,6 +6946,10 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
                     label: 'Sterowanie',
                     description: 'Dla organizacji z zaawansowanymi wymaganiami.',
                 },
+                submit2026: {
+                    label: 'Prześlij',
+                    description: 'Dla pracowników, którzy chcą przesyłać wydatki do pracodawcy.',
+                },
             },
             description: 'Wybierz plan odpowiedni dla siebie. Szczegółową listę funkcji i cen znajdziesz w naszej',
             subscriptionLink: 'strona pomocy dotycząca typów planów i cen',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 83878b0ec8b9..ffbfa26b6e8f 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -6952,6 +6952,10 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
                     label: 'Controle',
                     description: 'Para organizações com requisitos avançados.',
                 },
+                submit2026: {
+                    label: 'Enviar',
+                    description: 'Para funcionários que desejam enviar despesas ao empregador.',
+                },
             },
             description: 'Escolha o plano ideal para você. Para ver a lista detalhada de recursos e preços, confira nosso',
             subscriptionLink: 'página de ajuda sobre tipos de plano e preços',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index beb5b27b0ad3..9fcc25c15598 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -6779,6 +6779,10 @@ ${reportName}
                     label: '控制',
                     description: '适用于具有高级需求的组织。',
                 },
+                submit2026: {
+                    label: '提交',
+                    description: '适用于希望向雇主提交费用的员工。',
+                },
             },
             description: '选择适合您的方案。要查看详细的功能和价格列表，请访问我们的',
             subscriptionLink: '方案类型和价格帮助页面',
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 8e3866794b98..1291d1131c6d 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -879,6 +879,32 @@ function isPaidGroupPolicy(policy: OnyxInputOrEntry<Policy>): boolean {
     return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE;
 }
 
+function isSubmitPolicy(policy: OnyxInputOrEntry<Policy>): boolean {
+    return policy?.type === CONST.POLICY.TYPE.SUBMIT;
+}
+
+function isPolicyEditor(policy: OnyxEntry<Policy>): boolean {
+    return policy?.role === CONST.POLICY.ROLE.EDITOR;
+}
+
+/**
+ * Returns true if the user can edit workspace settings — admins on any workspace, or editors on Submit workspaces.
+ */
+function canEditWorkspaceSettings(policy: OnyxEntry<Policy>): boolean {
+    return isPolicyAdmin(policy) || isPolicyEditor(policy);
+}
+
+/**
+ * Returns true for any group workspace: paid (Team/Corporate) or Submit.
+ *
+ * Note: not to be confused with `ReportUtils.isGroupPolicy(policyType: string)`,
+ * which excludes Submit. Use this helper when Submit workspaces should be treated
+ * like paid workspaces (e.g. access gating for shared workspace pages).
+ */
+function isGroupPolicy(policy: OnyxInputOrEntry<Policy>): boolean {
+    return isPaidGroupPolicy(policy) || isSubmitPolicy(policy);
+}
+
 function getOwnedPaidPolicies(policies: OnyxCollection<Policy> | null, currentUserAccountID: number | undefined): Policy[] {
     return Object.values(policies ?? {}).filter((policy): policy is Policy => isPolicyOwner(policy, currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID) && isPaidGroupPolicy(policy));
 }
@@ -2177,6 +2203,10 @@ export {
     isDelayedSubmissionEnabled,
     getCorrectedAutoReportingFrequency,
     isPaidGroupPolicy,
+    isSubmitPolicy,
+    isPolicyEditor,
+    canEditWorkspaceSettings,
+    isGroupPolicy,
     isPendingDeletePolicy,
     isPolicyAdmin,
     isPolicyUser,
diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts
index 7193ad676243..3c26d554354d 100644
--- a/src/libs/SubscriptionUtils.ts
+++ b/src/libs/SubscriptionUtils.ts
@@ -539,7 +539,7 @@ function getSubscriptionPrice(
     privateSubscriptionType: SubscriptionType | undefined,
     hasTeam2025Pricing: boolean,
 ): number {
-    if (!privateSubscriptionType || !plan) {
+    if (!privateSubscriptionType || !plan || plan === CONST.POLICY.TYPE.SUBMIT) {
         return 0;
     }
 
diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts
index 4c8704f37d83..a59721afb0ca 100644
--- a/src/libs/actions/Policy/Member.ts
+++ b/src/libs/actions/Policy/Member.ts
@@ -1083,28 +1083,55 @@ function inviteMemberToWorkspace(policyID: string, inviterEmail?: string) {
 }
 
 /**
- * Add member to the selected private domain workspace based on policyID
+ * Add member to the selected private domain workspace based on policyID.
+ *
+ * The optimistic merge is intentionally limited to `isLoading: true`. We can't
+ * tell the policy's real type/role from the joinable-policy payload (it's not
+ * exposed on `JoinablePolicy`), so writing speculative `type: SUBMIT` /
+ * `role: EDITOR` would corrupt Team/Corporate policies joined via the same
+ * private-domain flow. The `isLoading` flag is enough to suppress the brief
+ * NotFoundPage flash in `WorkspaceInitialPage` / `AccessOrNotFoundWrapper`
+ * until the backend response hydrates the policy with its actual shape.
  */
 function joinAccessiblePolicy(policyID: string) {
     const memberJoinKey = `${ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER}${policyID}` as const;
+    const policyKey = `${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const;
 
-    const optimisticData: Array<OnyxUpdate<typeof memberJoinKey>> = [
+    const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER | typeof ONYXKEYS.COLLECTION.POLICY>> = [
         {
             onyxMethod: Onyx.METHOD.MERGE,
             key: memberJoinKey,
             value: {policyID},
         },
+        {
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: policyKey,
+            value: {isLoading: true},
+        },
     ];
 
-    const failureData: Array<OnyxUpdate<typeof memberJoinKey>> = [
+    const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY>> = [
+        {
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: policyKey,
+            value: {isLoading: false},
+        },
+    ];
+
+    const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER | typeof ONYXKEYS.COLLECTION.POLICY>> = [
         {
             onyxMethod: Onyx.METHOD.MERGE,
             key: memberJoinKey,
             value: {policyID, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd')},
         },
+        {
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: policyKey,
+            value: {isLoading: false},
+        },
     ];
 
-    API.write(WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY, {policyID}, {optimisticData, failureData});
+    API.write(WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY, {policyID}, {optimisticData, successData, failureData});
 }
 
 /**
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 28223c560537..49e3c7eb9cca 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -214,7 +214,7 @@ type BuildPolicyDataOptions = {
     onboardingPurposeSelected?: OnboardingPurpose;
     shouldAddGuideWelcomeMessage?: boolean;
     shouldCreateControlPolicy?: boolean;
-    type?: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE;
+    type?: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.SUBMIT;
     // TODO: Make it required once we complete refactoring the buildPolicyData function to use isSelfTourViewed. Refactor issue: https://github.com/Expensify/App/issues/66424
     isSelfTourViewed?: boolean;
     betas: OnyxEntry<Beta[]>;
@@ -2290,7 +2290,7 @@ function createDraftInitialWorkspace(
     makeMeAdmin = false,
     currency = '',
     file?: File,
-    type: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE = CONST.POLICY.TYPE.TEAM,
+    type: typeof CONST.POLICY.TYPE.TEAM | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.SUBMIT = CONST.POLICY.TYPE.TEAM,
     isAnnualSubscription = false,
 ) {
     const {customUnits, outputCurrency} = buildOptimisticDistanceRateCustomUnits(currency);
@@ -2367,6 +2367,27 @@ type BuildPolicyDataKeys =
     | typeof ONYXKEYS.NVP_LAST_PAYMENT_METHOD
     | typeof ONYXKEYS.PERSONAL_DETAILS_LIST;
 
+function getRoleForNewWorkspaceMember(isSubmitWorkspace: boolean, makeMeAdmin: boolean): ValueOf<typeof CONST.POLICY.ROLE> {
+    if (isSubmitWorkspace) {
+        return CONST.POLICY.ROLE.EDITOR;
+    }
+    return makeMeAdmin ? CONST.POLICY.ROLE.ADMIN : CONST.POLICY.ROLE.USER;
+}
+
+function getApprovalModeForNewWorkspace(
+    isSubmitWorkspace: boolean,
+    shouldEnableWorkflowsByDefault: boolean,
+    engagementChoice?: OnboardingPurpose,
+): ValueOf<typeof CONST.POLICY.APPROVAL_MODE> {
+    if (isSubmitWorkspace) {
+        return CONST.POLICY.APPROVAL_MODE.ADVANCED;
+    }
+    if (shouldEnableWorkflowsByDefault && engagementChoice !== CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE) {
+        return CONST.POLICY.APPROVAL_MODE.BASIC;
+    }
+    return CONST.POLICY.APPROVAL_MODE.OPTIONAL;
+}
+
 /**
  * Generates onyx data for creating a new workspace
  *
@@ -2429,7 +2450,9 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
     const optimisticCategoriesData = buildOptimisticPolicyCategories(policyID, Object.values(CONST.POLICY.DEFAULT_CATEGORIES));
     const optimisticMccGroupData = buildOptimisticMccGroup();
 
+    const isSubmitWorkspace = type === CONST.POLICY.TYPE.SUBMIT;
     const shouldEnableWorkflowsByDefault =
+        isSubmitWorkspace ||
         !engagementChoice ||
         engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ||
         engagementChoice === CONST.ONBOARDING_CHOICES.LOOKING_AROUND ||
@@ -2443,7 +2466,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
 
     const workspaceType = type ?? (isCorporateFeature || isCorporateIntegration || shouldCreateControlPolicy || isAnnualSubscription ? CONST.POLICY.TYPE.CORPORATE : CONST.POLICY.TYPE.TEAM);
 
-    const areDistanceRatesEnabled = !!featuresMap?.find((feature) => feature.id === CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED && feature.enabled);
+    const areDistanceRatesEnabled = isSubmitWorkspace || !!featuresMap?.find((feature) => feature.id === CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED && feature.enabled);
 
     // WARNING: The data below should be kept in sync with the API so we create the policy with the correct configuration.
     const optimisticData: Array<
@@ -2469,7 +2492,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
                 id: policyID,
                 type: workspaceType,
                 name: policyName,
-                role: CONST.POLICY.ROLE.ADMIN,
+                role: isSubmitWorkspace ? CONST.POLICY.ROLE.EDITOR : CONST.POLICY.ROLE.ADMIN,
                 owner: policyOwnerEmail || currentUserEmailParam,
                 ownerAccountID: policyOwnerEmail ? (PersonalDetailsUtils.getPersonalDetailByEmail(policyOwnerEmail)?.accountID ?? currentUserAccountIDParam) : currentUserAccountIDParam,
                 isPolicyExpenseChatEnabled: true,
@@ -2478,8 +2501,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
                 autoReporting: true,
                 approver: currentUserEmailParam,
                 autoReportingFrequency: shouldEnableWorkflowsByDefault ? CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE : CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT,
-                approvalMode:
-                    shouldEnableWorkflowsByDefault && engagementChoice !== CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE ? CONST.POLICY.APPROVAL_MODE.BASIC : CONST.POLICY.APPROVAL_MODE.OPTIONAL,
+                approvalMode: getApprovalModeForNewWorkspace(isSubmitWorkspace, shouldEnableWorkflowsByDefault, engagementChoice),
                 harvesting: {
                     enabled: !shouldEnableWorkflowsByDefault,
                 },
@@ -2490,8 +2512,8 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
                 created: DateUtils.getDBTime(),
                 customUnits,
                 areCategoriesEnabled: true,
-                areCompanyCardsEnabled: true,
-                areTagsEnabled: false,
+                areCompanyCardsEnabled: !isSubmitWorkspace,
+                areTagsEnabled: isSubmitWorkspace,
                 areDistanceRatesEnabled,
                 areWorkflowsEnabled: shouldEnableWorkflowsByDefault,
                 areReportFieldsEnabled: false,
@@ -2503,7 +2525,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
                               [policyOwnerEmail]: {
                                   submitsTo: policyOwnerEmail,
                                   email: policyOwnerEmail,
-                                  role: CONST.POLICY.ROLE.ADMIN,
+                                  role: isSubmitWorkspace ? CONST.POLICY.ROLE.EDITOR : CONST.POLICY.ROLE.ADMIN,
                                   errors: {},
                               },
                           }
@@ -2513,7 +2535,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
                               [currentUserEmailParam]: {
                                   submitsTo: currentUserEmailParam,
                                   email: currentUserEmailParam,
-                                  role: CONST.POLICY.ROLE.ADMIN,
+                                  role: isSubmitWorkspace ? CONST.POLICY.ROLE.EDITOR : CONST.POLICY.ROLE.ADMIN,
                                   errors: {},
                               },
                           }
@@ -2523,7 +2545,7 @@ function buildPolicyData(options: BuildPolicyDataOptions): OnyxData<BuildPolicyD
                               [currentUserEmailParam]: {
                                   submitsTo: policyOwnerEmail,
                                   email: currentUserEmailParam,
-                                  role: makeMeAdmin ? CONST.POLICY.ROLE.ADMIN : CONST.POLICY.ROLE.USER,
+                                  role: getRoleForNewWorkspaceMember(isSubmitWorkspace, makeMeAdmin),
                                   errors: {},
                               },
                           }
diff --git a/src/libs/actions/Welcome/index.ts b/src/libs/actions/Welcome/index.ts
index 252f6a83a04e..43ea20ca680d 100644
--- a/src/libs/actions/Welcome/index.ts
+++ b/src/libs/actions/Welcome/index.ts
@@ -13,6 +13,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import type {OnboardingPurpose} from '@src/types/onyx';
 import type Onboarding from '@src/types/onyx/Onboarding';
+import type OnboardingRHPVariant from '@src/types/onyx/OnboardingRHPVariant';
 import type {OnboardingCompanySize} from './OnboardingFlow';
 
 let isLoadingReportData = true;
@@ -65,6 +66,10 @@ function setOnboardingPolicyID(policyID?: string) {
     Onyx.set(ONYXKEYS.ONBOARDING_POLICY_ID, policyID ?? null);
 }
 
+function setOnboardingRHPVariant(value?: OnboardingRHPVariant) {
+    Onyx.set(ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, value ?? null);
+}
+
 function updateOnboardingLastVisitedPath(path: string) {
     Onyx.merge(ONYXKEYS.ONBOARDING_LAST_VISITED_PATH, path);
 }
@@ -186,6 +191,7 @@ export {
     resetAllChecks,
     setOnboardingAdminsChatReportID,
     setOnboardingPolicyID,
+    setOnboardingRHPVariant,
     completeHybridAppOnboarding,
     setOnboardingErrorMessage,
     setOnboardingCompanySize,
diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts
index 509f7928c7c1..1b78b0a4a2a8 100644
--- a/src/libs/navigateAfterOnboarding.ts
+++ b/src/libs/navigateAfterOnboarding.ts
@@ -6,6 +6,8 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import type {OnboardingRHPVariant} from '@src/types/onyx';
 import {setDisableDismissOnEscape} from './actions/Modal';
+import SidePanelActions from './actions/SidePanel';
+import {setOnboardingRHPVariant} from './actions/Welcome';
 import shouldOpenOnAdminRoom from './Navigation/helpers/shouldOpenOnAdminRoom';
 import Navigation from './Navigation/Navigation';
 import {findLastAccessedReport, isConciergeChatReport, isSelfDM} from './ReportUtils';
@@ -124,4 +126,32 @@ function navigateAfterOnboardingWithMicrotaskQueue(
     });
 }
 
-export {navigateAfterOnboarding, navigateAfterOnboardingWithMicrotaskQueue};
+/**
+ * After creating or joining a Submit workspace during onboarding,
+ * navigate to Workspace > Categories with the side panel open so
+ * the #admins room is visible in Concierge Anywhere.
+ */
+function navigateToSubmitWorkspaceAfterOnboarding(policyID?: string, isSmallScreenWidth = false) {
+    setDisableDismissOnEscape(false);
+
+    if (!policyID) {
+        Navigation.navigate(ROUTES.HOME);
+        return;
+    }
+
+    setOnboardingRHPVariant(CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM);
+    Navigation.navigate(ROUTES.WORKSPACES_LIST.route);
+    Navigation.setNavigationActionToMicrotaskQueue(() => {
+        Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID));
+        SidePanelActions.openSidePanel(!isSmallScreenWidth);
+    });
+}
+
+function navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyID?: string, isSmallScreenWidth = false) {
+    Navigation.dismissModal();
+    Navigation.setNavigationActionToMicrotaskQueue(() => {
+        navigateToSubmitWorkspaceAfterOnboarding(policyID, isSmallScreenWidth);
+    });
+}
+
+export {navigateAfterOnboarding, navigateAfterOnboardingWithMicrotaskQueue, navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue};
diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
index 85b3e67b4bc8..956f91be9169 100644
--- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
+++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
@@ -10,6 +10,7 @@ import Text from '@components/Text';
 import TextInput from '@components/TextInput';
 import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
 import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
+import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace';
 import useAutoCreateTrackWorkspace from '@hooks/useAutoCreateTrackWorkspace';
 import useAutoFocusInput from '@hooks/useAutoFocusInput';
 import useLocalize from '@hooks/useLocalize';
@@ -54,6 +55,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
     const [onboardingPersonalDetailsForm] = useOnyx(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM);
     const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
     const autoCreateTrackWorkspace = useAutoCreateTrackWorkspace();
+    const autoCreateSubmitWorkspace = useAutoCreateSubmitWorkspace();
 
     // When we merge public email with work email, we now want to navigate to the
     // concierge chat report of the new work email and not the last accessed report.
@@ -64,6 +66,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
     const {inputCallbackRef} = useAutoFocusInput();
     const [shouldValidateOnChange, setShouldValidateOnChange] = useState(false);
     const {isBetaEnabled} = usePermissions();
+    const canUseSubmit2026 = isBetaEnabled(CONST.BETAS.SUBMIT_2026);
     const onboardingStep = useOnboardingStepCounter(SCREENS.ONBOARDING.PERSONAL_DETAILS);
 
     const isPrivateDomainAndHasAccessiblePolicies = !account?.isFromPublicDomain && !!account?.hasAccessibleDomainPolicies;
@@ -131,6 +134,16 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
             clearPersonalDetailsDraft();
             setPersonalDetails(firstName, lastName);
 
+            if (onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.EMPLOYER && canUseSubmit2026) {
+                if (isPrivateDomainAndHasAccessiblePolicies && isValidated) {
+                    Navigation.navigate(ROUTES.ONBOARDING_WORKSPACES.getRoute(route.params?.backTo));
+                    return;
+                }
+                updateDisplayName(firstName, lastName, formatPhoneNumber, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? '');
+                autoCreateSubmitWorkspace(firstName, lastName);
+                return;
+            }
+
             if (isPrivateDomainAndHasAccessiblePolicies && (!onboardingPurposeSelected || isVsb || isSmb)) {
                 const nextRoute = isValidated ? ROUTES.ONBOARDING_WORKSPACES : ROUTES.ONBOARDING_PRIVATE_DOMAIN;
                 Navigation.navigate(nextRoute.getRoute(route.params?.backTo));
@@ -157,12 +170,14 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat
             session?.email,
             isPrivateDomainAndHasAccessiblePolicies,
             onboardingPurposeSelected,
+            canUseSubmit2026,
             isVsb,
             isSmb,
             completeOnboarding,
             isValidated,
             route.params?.backTo,
             autoCreateTrackWorkspace,
+            autoCreateSubmitWorkspace,
         ],
     );
 
diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
index 2643a641699a..c33ec373f627 100644
--- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
+++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
@@ -9,18 +9,21 @@ import type {MenuItemProps} from '@components/MenuItem';
 import MenuItemList from '@components/MenuItemList';
 import ScreenWrapper from '@components/ScreenWrapper';
 import Text from '@components/Text';
+import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace';
 import useAutoCreateTrackWorkspace from '@hooks/useAutoCreateTrackWorkspace';
 import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useOnboardingMessages from '@hooks/useOnboardingMessages';
 import useOnboardingStepCounter from '@hooks/useOnboardingStepCounter';
 import useOnyx from '@hooks/useOnyx';
+import usePermissions from '@hooks/usePermissions';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
 import OnboardingRefManager from '@libs/OnboardingRefManager';
 import type {TOnboardingRef} from '@libs/OnboardingRefManager';
+import {isCurrentUserValidated} from '@libs/UserUtils';
 import variables from '@styles/variables';
 import {completeOnboarding} from '@userActions/Report';
 import {setOnboardingErrorMessage, setOnboardingPurposeSelected} from '@userActions/Welcome';
@@ -74,6 +77,12 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
     const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
     const [betas] = useOnyx(ONYXKEYS.BETAS);
+    const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST);
+    const [session] = useOnyx(ONYXKEYS.SESSION);
+    const {isBetaEnabled} = usePermissions();
+    const canUseSubmit2026 = isBetaEnabled(CONST.BETAS.SUBMIT_2026);
+    const isValidated = isCurrentUserValidated(loginList, session?.email);
+    const autoCreateSubmitWorkspace = useAutoCreateSubmitWorkspace();
     const autoCreateTrackWorkspace = useAutoCreateTrackWorkspace();
     const paddingHorizontal = onboardingIsMediumOrLargerScreenWidth ? styles.ph8 : styles.ph5;
 
@@ -102,6 +111,23 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, ro
                     return;
                 }
 
+                if (choice === CONST.ONBOARDING_CHOICES.EMPLOYER && canUseSubmit2026) {
+                    if (isPrivateDomainAndHasAccessiblePolicies && isValidated) {
+                        Navigation.navigate(
+                            personalDetailsForm?.firstName ? ROUTES.ONBOARDING_WORKSPACES.getRoute(route.params?.backTo) : ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute(route.params?.backTo),
+                        );
+                        return;
+                    }
+
+                    if (personalDetailsForm?.firstName) {
+                        autoCreateSubmitWorkspace(personalDetailsForm.firstName, personalDetailsForm.lastName ?? '');
+                        return;
+                    }
+
+                    Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute(route.params?.backTo));
+                    return;
+                }
+
                 if (isPrivateDomainAndHasAccessiblePolicies && personalDetailsForm?.firstName) {
                     if (choice === CONST.ONBOARDING_CHOICES.PERSONAL_SPEND) {
                         autoCreateTrackWorkspace(personalDetailsForm.firstName, personalDetailsForm.lastName ?? '', choice);
diff --git a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx
index b91ad2e1a154..e11b1db42c91 100644
--- a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx
+++ b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx
@@ -9,6 +9,7 @@ import SelectionList from '@components/SelectionList';
 import UserListItem from '@components/SelectionList/ListItem/UserListItem';
 import Text from '@components/Text';
 import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet';
+import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace';
 import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
@@ -19,7 +20,7 @@ import usePermissions from '@hooks/usePermissions';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
-import {navigateAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding';
+import {navigateAfterOnboardingWithMicrotaskQueue, navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding';
 import Navigation from '@libs/Navigation/Navigation';
 import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';
 import {isCurrentUserValidated} from '@libs/UserUtils';
@@ -69,20 +70,34 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding
     const [onboardingValues] = useOnyx(ONYXKEYS.NVP_ONBOARDING);
     const isVsb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.VSB;
     const isSmb = onboardingValues?.signupQualifier === CONST.ONBOARDING_SIGNUP_QUALIFIERS.SMB;
+    const [onboardingPurposeSelected] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED);
+    const canUseSubmit2026 = isBetaEnabled(CONST.BETAS.SUBMIT_2026);
+    const isEmployerWithSubmit = onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.EMPLOYER && canUseSubmit2026;
+    const autoCreateSubmitWorkspace = useAutoCreateSubmitWorkspace();
     const shouldHideBackButton = onboardingValues?.shouldValidate === false && route.params?.backTo === ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute();
     const onboardingStep = useOnboardingStepCounter(SCREENS.ONBOARDING.WORKSPACES);
 
     const handleJoinWorkspace = (policy: JoinablePolicy) => {
+        // Only mirror the EMPLOYER ("Get paid back by my employer") + Submit-2026 onboarding flow
+        // when the user actually picked EMPLOYER, or when no Purpose was selected (private-domain
+        // users who reach this screen without going through the Purpose step). Users on the Submit
+        // beta who picked a different Purpose (e.g. MANAGE_TEAM) must not be re-routed through
+        // the Submit flow.
+        const shouldUseSubmitFlow = canUseSubmit2026 && (!onboardingPurposeSelected || onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.EMPLOYER);
+
         if (policy.automaticJoiningEnabled) {
             joinAccessiblePolicy(policy.policyID);
         } else {
             askToJoinPolicy(policy.policyID);
         }
+
+        const engagementChoice = shouldUseSubmitFlow ? CONST.ONBOARDING_CHOICES.EMPLOYER : CONST.ONBOARDING_CHOICES.LOOKING_AROUND;
         completeOnboarding({
-            engagementChoice: CONST.ONBOARDING_CHOICES.LOOKING_AROUND,
-            onboardingMessage: onboardingMessages[CONST.ONBOARDING_CHOICES.LOOKING_AROUND],
+            engagementChoice,
+            onboardingMessage: onboardingMessages[engagementChoice],
             firstName: onboardingPersonalDetails?.firstName ?? '',
             lastName: onboardingPersonalDetails?.lastName ?? '',
+            onboardingPolicyID: shouldUseSubmitFlow && policy.automaticJoiningEnabled ? policy.policyID : undefined,
             companySize: onboardingCompanySize,
             introSelected,
             isSelfTourViewed,
@@ -91,6 +106,11 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding
         setOnboardingAdminsChatReportID();
         setOnboardingPolicyID(policy.policyID);
 
+        if (shouldUseSubmitFlow && policy.automaticJoiningEnabled) {
+            navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policy.policyID, isSmallScreenWidth);
+            return;
+        }
+
         navigateAfterOnboardingWithMicrotaskQueue(
             isSmallScreenWidth,
             isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS),
@@ -146,6 +166,11 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding
     });
 
     const skipJoiningWorkspaces = () => {
+        if (isEmployerWithSubmit) {
+            autoCreateSubmitWorkspace(onboardingPersonalDetails?.firstName ?? '', onboardingPersonalDetails?.lastName ?? '');
+            return;
+        }
+
         if (isVsb) {
             Navigation.navigate(ROUTES.ONBOARDING_ACCOUNTING.getRoute(route.params?.backTo));
             return;
diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
index 87fab021f830..636662d1062d 100644
--- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
@@ -14,7 +14,7 @@ import {openWorkspace} from '@libs/actions/Policy/Policy';
 import {isValidMoneyRequestType} from '@libs/IOUUtils';
 import goBackFromWorkspaceSettingPages from '@libs/Navigation/helpers/goBackFromWorkspaceSettingPages';
 import Navigation from '@libs/Navigation/Navigation';
-import {canSendInvoice, isControlPolicy, isPaidGroupPolicy, isPolicyAccessible, isPolicyAdmin, isPolicyFeatureEnabled as isPolicyFeatureEnabledUtil} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, canSendInvoice, isControlPolicy, isGroupPolicy, isPolicyAccessible, isPolicyFeatureEnabled as isPolicyFeatureEnabledUtil} from '@libs/PolicyUtils';
 import {canCreateRequest} from '@libs/ReportUtils';
 import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
 import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
@@ -30,9 +30,9 @@ import callOrReturn from '@src/types/utils/callOrReturn';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
 
 const ACCESS_VARIANTS = {
-    [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry<Policy>) => isPaidGroupPolicy(policy),
+    [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry<Policy>) => isGroupPolicy(policy),
     [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry<Policy>) => isControlPolicy(policy),
-    [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry<Policy>, login: string) => isPolicyAdmin(policy, login),
+    [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry<Policy>) => canEditWorkspaceSettings(policy),
     [CONST.IOU.ACCESS_VARIANTS.CREATE]: (
         policy: OnyxEntry<Policy>,
         login: string,
@@ -164,7 +164,8 @@ function AccessOrNotFoundWrapper({
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [isPolicyIDInRoute, policyID]);
 
-    const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id);
+    const isPolicyEmpty = !Object.entries(policy ?? {}).length || !policy?.id;
+    const shouldShowFullScreenLoadingIndicator = !isMoneyRequest && (isLoadingReportData !== false || !!policy?.isLoading) && isPolicyEmpty;
 
     const isFeatureEnabled = featureName ? isPolicyFeatureEnabledUtil(policy, featureName) : true;
 
diff --git a/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx b/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx
index 8ad8ac82a280..4d40137e051d 100644
--- a/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx
+++ b/src/pages/workspace/DynamicWorkspaceOverviewPlanTypePage.tsx
@@ -55,7 +55,17 @@ function DynamicWorkspaceOverviewPlanTypePage({policy}: WithPolicyProps) {
     }, [policy?.type]);
 
     const workspacePlanTypes = Object.values(CONST.POLICY.TYPE)
-        .filter((type) => type !== CONST.POLICY.TYPE.PERSONAL)
+        .filter((type) => {
+            if (type === CONST.POLICY.TYPE.PERSONAL) {
+                return false;
+            }
+            // Guard: don't leak the SUBMIT plan type into the plan-type list for paid workspaces.
+            // Submit-specific plan-type UX (exposing SUBMIT for Submit policies) ships in #87263.
+            if (type === CONST.POLICY.TYPE.SUBMIT) {
+                return false;
+            }
+            return true;
+        })
         .map<WorkspacePlanTypeItem>((policyType) => ({
             value: policyType,
             text: translate(`workspace.planTypePage.planTypes.${policyType as PersonalPolicyTypeExcludedProps}.label`),
@@ -81,6 +91,14 @@ function DynamicWorkspaceOverviewPlanTypePage({policy}: WithPolicyProps) {
         ) : null;
 
     const handleUpdatePlan = () => {
+        // Submit policies don't expose SUBMIT in the option list, but the editor can
+        // still pick Team/Corporate. Route any selection from a Submit policy to the
+        // upgrade screen — the polished Submit-specific upgrade UX ships in #87263.
+        if (policyID && policy?.type === CONST.POLICY.TYPE.SUBMIT && (currentPlan === CONST.POLICY.TYPE.TEAM || currentPlan === CONST.POLICY.TYPE.CORPORATE)) {
+            Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID));
+            return;
+        }
+
         if (policyID && policy?.type === CONST.POLICY.TYPE.TEAM && currentPlan === CONST.POLICY.TYPE.CORPORATE) {
             Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID));
             return;
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index ad9bc4901466..2a1d8c17ff04 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -14,7 +14,6 @@ import ScrollView from '@components/ScrollView';
 import useCardFeedErrors from '@hooks/useCardFeedErrors';
 import useConfirmReadyToOpenApp from '@hooks/useConfirmReadyToOpenApp';
 import {useCurrencyListActions} from '@hooks/useCurrencyList';
-import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
 import useGetReceiptPartnersIntegrationData from '@hooks/useGetReceiptPartnersIntegrationData';
 import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
@@ -33,14 +32,14 @@ import {clearErrors, openPolicyInitialPage, removeWorkspace} from '@libs/actions
 import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import {
+    canEditWorkspaceSettings,
     canPolicyAccessFeature,
     shouldShowPolicy as checkIfShouldShowPolicy,
     goBackFromInvalidPolicy,
     hasAccountingFeatureConnection,
     hasPolicyCategoriesError,
-    isPaidGroupPolicy,
+    isGroupPolicy,
     isPendingDeletePolicy,
-    isPolicyAdmin,
     isTimeTrackingEnabled,
     shouldShowEmployeeListError,
     shouldShowSyncError,
@@ -95,7 +94,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
     const {translate} = useLocalize();
     const {convertToDisplayString} = useCurrencyListActions();
     const {isBetaEnabled} = usePermissions();
-    const {login} = useCurrentUserPersonalDetails();
+
     const isFocused = useIsFocused();
     const activeRoute = useNavigationState((state) => findFocusedRoute(state)?.name);
     const waitForNavigate = useWaitForNavigation();
@@ -134,7 +133,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
 
     const policyName = policy?.name ?? '';
     const hasPolicyCreationError = policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors);
-    const shouldShowProtectedItems = isPolicyAdmin(policy, login);
+    const shouldShowProtectedItems = canEditWorkspaceSettings(policy);
+
     const accountingConnectionNames = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES;
     const hasSyncError = shouldShowSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy), accountingConnectionNames);
     const hasMembersError = shouldShowEmployeeListError(policy);
@@ -184,8 +184,9 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
     const shouldShowPolicy = checkIfShouldShowPolicy(policy, true, currentUserLogin);
     const isPendingDelete = isPendingDeletePolicy(policy);
     const prevIsPendingDelete = isPendingDeletePolicy(prevPolicy);
-
-    const shouldShowNotFoundPage = !shouldShowPolicy && (!isPendingDelete || prevIsPendingDelete);
+    // While the policy is being fetched (e.g., right after joinAccessiblePolicy), the role is not yet populated,
+    // so checkIfShouldShowPolicy returns false. Suppress NotFound during this loading window.
+    const shouldShowNotFoundPage = !shouldShowPolicy && !policy?.isLoading && (!isPendingDelete || prevIsPendingDelete);
     const fetchPolicyData = () => {
         if (policyDraft?.id || !isFocused) {
             return;
@@ -227,7 +228,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
         },
     ];
 
-    if (isPaidGroupPolicy(policy) && shouldShowProtectedItems) {
+    if (isGroupPolicy(policy) && shouldShowProtectedItems) {
         workspaceMenuItems.push({
             translationKey: 'common.reports',
             icon: expensifyIcons.Document,
@@ -451,7 +452,10 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac
             >
                 <HeaderWithBackButton
                     title={policyName}
-                    onBackButtonPress={() => Navigation.goBack(route.params?.backTo ?? ROUTES.WORKSPACES_LIST.route)}
+                    onBackButtonPress={() =>
+                        // On narrow, goBack() pops the workspace split; goBack with a route can't resolve inside TAB_NAVIGATOR.
+                        shouldUseNarrowLayout && !route.params?.backTo ? Navigation.goBack() : Navigation.goBack(route.params?.backTo ?? ROUTES.WORKSPACES_LIST.route)
+                    }
                     policyAvatar={policyAvatar}
                     shouldDisplayHelpButton={shouldUseNarrowLayout}
                 />
diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx
index 33473c1f8c3d..c4db802bf574 100644
--- a/src/pages/workspace/WorkspaceMembersPage.tsx
+++ b/src/pages/workspace/WorkspaceMembersPage.tsx
@@ -62,13 +62,13 @@ import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
 import {isPersonalDetailsReady, sortAlphabetically} from '@libs/OptionsListUtils';
 import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils';
 import {
+    canEditWorkspaceSettings,
     getConnectionExporters,
     getMemberAccountIDsForWorkspace,
     isControlPolicy,
     isDeletedPolicyEmployee,
     isExpensifyTeam,
     isPaidGroupPolicy,
-    isPolicyAdmin as isPolicyAdminUtils,
     isPolicyApprover,
 } from '@libs/PolicyUtils';
 import {getDisplayNameForParticipant} from '@libs/ReportUtils';
@@ -155,7 +155,7 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers
     // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal
     // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
     const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
-    const isPolicyAdmin = isPolicyAdminUtils(policy);
+    const isPolicyAdmin = canEditWorkspaceSettings(policy);
     const isLoading = useMemo(
         () => !isOfflineAndNoMemberDataAvailable && (!isPersonalDetailsReady(personalDetails) || isEmptyObject(policy?.employeeList)),
         [isOfflineAndNoMemberDataAvailable, personalDetails, policy?.employeeList],
diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx
index 445882b87b8b..4f8d0cec2cc7 100644
--- a/src/pages/workspace/WorkspaceOverviewPage.tsx
+++ b/src/pages/workspace/WorkspaceOverviewPage.tsx
@@ -53,6 +53,7 @@ import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
 import {
+    canEditWorkspaceSettings,
     getConnectionExporters,
     getUserFriendlyWorkspaceType,
     goBackFromInvalidPolicy,
@@ -109,7 +110,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa
     const isBankAccountVerified = !!settings?.paymentBankAccountID;
     const shouldBlockCurrencyChange = useShouldBlockCurrencyChange(policyID);
 
-    const isPolicyAdmin = isPolicyAdminPolicyUtils(policy);
+    const isPolicyAdmin = canEditWorkspaceSettings(policy);
     const outputCurrency = policy?.outputCurrency ?? '';
     const currencySymbol = getCurrencySymbol(outputCurrency) ?? '';
     const formattedCurrency = !isEmptyObject(policy) ? `${outputCurrency} - ${currencySymbol}` : '';
@@ -177,7 +178,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa
     const policyName = policy?.name ?? '';
     const policyDescription = policy?.description ?? translate('workspace.common.defaultDescription');
     const policyCurrency = policy?.outputCurrency ?? '';
-    const readOnly = !isPolicyAdminPolicyUtils(policy);
+    const readOnly = !canEditWorkspaceSettings(policy);
     const currencyReadOnly = readOnly || isBankAccountVerified;
     const isOwner = isPolicyOwner(policy, currentUserPersonalDetails.accountID);
     const shouldShowAddress = !readOnly || !!formattedAddress;
diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx
index a2e1c8846792..1d90f6414c89 100644
--- a/src/pages/workspace/WorkspacePageWithSections.tsx
+++ b/src/pages/workspace/WorkspacePageWithSections.tsx
@@ -19,7 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
 import {openWorkspaceView} from '@libs/actions/BankAccounts';
 import goBackFromWorkspaceSettingPages from '@libs/Navigation/helpers/goBackFromWorkspaceSettingPages';
 import Navigation from '@libs/Navigation/Navigation';
-import {isPendingDeletePolicy, isPolicyAdmin, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, isPendingDeletePolicy, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils';
 import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -172,7 +172,7 @@ function WorkspacePageWithSections({
         }
 
         // We check isPendingDelete and prevIsPendingDelete to prevent the NotFound view from showing right after we delete the workspace
-        return (!isEmptyObject(policy) && !isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !(isPendingDelete && !prevIsPendingDelete));
+        return (!isEmptyObject(policy) && !canEditWorkspaceSettings(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !(isPendingDelete && !prevIsPendingDelete));
         // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [policy, shouldShowNonAdmin, shouldShowPolicy]);
 
diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
index 8661e2e0a8b4..576f4af16821 100644
--- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
+++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
@@ -15,7 +15,7 @@ import {updateXeroMappings} from '@libs/actions/connections/Xero';
 import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
-import {canModifyPlan, getDefaultApprover, getPerDiemCustomUnit, isControlPolicy} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, canModifyPlan, getDefaultApprover, getPerDiemCustomUnit, isControlPolicy} from '@libs/PolicyUtils';
 import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
 import {enablePerDiem} from '@userActions/Policy/PerDiem';
 import CONST from '@src/CONST';
@@ -231,7 +231,10 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
         }, [isUpgraded, canPerformUpgrade, confirmUpgrade]),
     );
 
-    if (!canPerformUpgrade) {
+    // Gate the page to users who can edit workspace settings (admins on any policy,
+    // or editors on Submit policies). `canPerformUpgrade` (strict admin) still controls
+    // whether the upgrade button is active, so editors see the intro but can't upgrade.
+    if (!canEditWorkspaceSettings(policy)) {
         return <NotFoundPage />;
     }
 
@@ -264,7 +267,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
                         policyID={policyID}
                         feature={feature}
                         onUpgrade={onUpgradeToCorporate}
-                        buttonDisabled={isOffline}
+                        buttonDisabled={isOffline || !canPerformUpgrade}
                         loading={policy?.isPendingUpgrade}
                         backTo={route.params.backTo}
                     />
diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx
index 0b94b5a3e8a7..b05076c6819b 100644
--- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx
@@ -14,7 +14,7 @@ import {getLatestErrorField} from '@libs/ErrorUtils';
 import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
-import {getCorrectedAutoReportingFrequency, goBackFromInvalidPolicy, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, getCorrectedAutoReportingFrequency, goBackFromInvalidPolicy, isGroupPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils';
 import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
 import withPolicy from '@pages/workspace/withPolicy';
 import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy';
@@ -118,7 +118,7 @@ function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoRepor
                 <FullPageNotFoundView
                     onBackButtonPress={goBackFromInvalidPolicy}
                     onLinkPress={goBackFromInvalidPolicy}
-                    shouldShow={isEmptyObject(policy) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy) || !isPaidGroupPolicy(policy)}
+                    shouldShow={isEmptyObject(policy) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy) || !isGroupPolicy(policy)}
                     subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'}
                     addBottomSafeAreaPadding
                 >
diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx
index 167c6cfeb448..5debd69369f1 100644
--- a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx
@@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize';
 import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
-import {goBackFromInvalidPolicy, isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, goBackFromInvalidPolicy, isGroupPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils';
 import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
 import withPolicy from '@pages/workspace/withPolicy';
 import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy';
@@ -94,7 +94,7 @@ function WorkspaceAutoReportingMonthlyOffsetPage({policy, route}: WorkspaceAutoR
                 <FullPageNotFoundView
                     onBackButtonPress={goBackFromInvalidPolicy}
                     onLinkPress={goBackFromInvalidPolicy}
-                    shouldShow={isEmptyObject(policy) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy) || !isPaidGroupPolicy(policy)}
+                    shouldShow={isEmptyObject(policy) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy) || !isGroupPolicy(policy)}
                     subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'}
                     addBottomSafeAreaPadding
                 >
diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
index 56618ac811b5..40fb435a3dc8 100644
--- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
@@ -49,10 +49,11 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig
 import {getPaymentMethodDescription} from '@libs/PaymentUtils';
 import {getDisplayNameOrDefault, getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
 import {
+    canEditWorkspaceSettings,
     getCorrectedAutoReportingFrequency,
     hasDynamicExternalWorkflow,
     isControlPolicy,
-    isPaidGroupPolicy as isPaidGroupPolicyUtil,
+    isGroupPolicy as isGroupPolicyUtil,
     isPolicyAdmin as isPolicyAdminUtil,
 } from '@libs/PolicyUtils';
 import {hasInProgressVBBA} from '@libs/ReimbursementAccountUtils';
@@ -630,7 +631,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
         </Section>
     );
 
-    const isPaidGroupPolicy = isPaidGroupPolicyUtil(policy);
+    const isGroupPolicy = isGroupPolicyUtil(policy);
     const isLoading = !!(policy?.isLoading && policy?.reimbursementChoice === undefined);
 
     return (
@@ -643,7 +644,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) {
                 icon={illustrations.Workflows}
                 route={route}
                 shouldShowOfflineIndicatorInWideScreen
-                shouldShowNotFoundPage={!isPaidGroupPolicy || !isPolicyAdmin}
+                shouldShowNotFoundPage={!isGroupPolicy || !canEditWorkspaceSettings(policy)}
                 isLoading={isLoading}
                 shouldShowLoading={isLoading}
                 shouldUseScrollView
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx
index 5e14701cac06..f7822beb595f 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsApprovalLimitPage.tsx
@@ -23,7 +23,7 @@ import {convertToBackendAmount, convertToFrontendAmountAsString} from '@libs/Cur
 import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
-import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, goBackFromInvalidPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils';
 import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
 import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
 import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -65,7 +65,7 @@ function WorkspaceWorkflowsApprovalsApprovalLimitPage({policy, isLoadingReportDa
     const selectedApproverPersonalDetails = selectedApproverEmail ? personalDetailsByEmail?.[selectedApproverEmail] : undefined;
     const selectedApproverDisplayName = selectedApproverEmail ? Str.removeSMSDomain(selectedApproverPersonalDetails?.displayName ?? selectedApproverEmail) : '';
 
-    const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy);
+    const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy);
 
     const approverDisplayName = currentApprover ? Str.removeSMSDomain(currentApprover.displayName) : '';
     const isApproverSelected = isEditFlow ? approverDisplayName.length > 0 : true;
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
index 15da8a38321d..2c81ebc003c4 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage.tsx
@@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
-import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, goBackFromInvalidPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils';
 import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
 import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
 import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -36,7 +36,7 @@ function WorkspaceWorkflowsApprovalsCreatePage({policy, isLoadingReportData = tr
     const [addExpenseApprovalsTaskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${addExpenseApprovalsTaskReportID}`);
     const formRef = useRef<ScrollView>(null);
 
-    const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy);
+    const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy);
 
     const createApprovalWorkflow = useCallback(() => {
         if (!approvalWorkflow) {
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
index 6ada3231860a..d43a130d3aa8 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage.tsx
@@ -18,7 +18,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
-import {goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, goBackFromInvalidPolicy, isPendingDeletePolicy} from '@libs/PolicyUtils';
 import {convertPolicyEmployeesToApprovalWorkflows, mergeWorkflowMembersWithAvailableMembers} from '@libs/WorkflowUtils';
 import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
 import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -102,7 +102,7 @@ function WorkspaceWorkflowsApprovalsEditPage({policy, isLoadingReportData = true
 
     const {currentApprovalWorkflow, defaultWorkflowMembers, usedApproverEmails} = getApprovalWorkflowData();
 
-    const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy) || !currentApprovalWorkflow;
+    const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy) || !currentApprovalWorkflow;
 
     // Set the initial approval workflow when the page is loaded
     useEffect(() => {
diff --git a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
index cc9f680177a7..dcb9a4639f43 100644
--- a/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
+++ b/src/pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsExpensesFromPage.tsx
@@ -15,7 +15,7 @@ import Navigation from '@libs/Navigation/Navigation';
 import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
 import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
 import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
-import {getDefaultApprover, getMemberAccountIDsForWorkspace, isPendingDeletePolicy, isPolicyAdmin} from '@libs/PolicyUtils';
+import {canEditWorkspaceSettings, getDefaultApprover, getMemberAccountIDsForWorkspace, isPendingDeletePolicy} from '@libs/PolicyUtils';
 import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
 import MemberRightIcon from '@pages/workspace/MemberRightIcon';
 import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -41,7 +41,7 @@ function WorkspaceWorkflowsApprovalsExpensesFromPage({policy, isLoadingReportDat
     const isLoadingApprovalWorkflow = isLoadingOnyxValue(approvalWorkflowResults);
     const [selectedMembers, setSelectedMembers] = useState<SelectionListApprover[]>([]);
 
-    const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || isPendingDeletePolicy(policy);
+    const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !canEditWorkspaceSettings(policy) || isPendingDeletePolicy(policy);
     const isInitialCreationFlow = approvalWorkflow?.action === CONST.APPROVAL_WORKFLOW.ACTION.CREATE && approvalWorkflow?.isInitialFlow;
     const shouldShowListEmptyContent = !isLoadingApprovalWorkflow && approvalWorkflow?.availableMembers.length === 0;
     const firstApprover = approvalWorkflow?.originalApprovers?.[0]?.email ?? '';
diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts
index 0aa0eacbd0fa..504e274532fc 100644
--- a/tests/actions/PolicyTest.ts
+++ b/tests/actions/PolicyTest.ts
@@ -1112,6 +1112,43 @@ describe('actions/Policy', () => {
             });
         });
 
+        it('creates a Submit workspace with ADVANCED approval mode and correct feature flags', async () => {
+            const policyID = Policy.generatePolicyID();
+            Policy.createWorkspace({
+                policyOwnerEmail: ESH_EMAIL,
+                makeMeAdmin: true,
+                policyName: WORKSPACE_NAME,
+                policyID,
+                engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                introSelected: {choice: CONST.ONBOARDING_CHOICES.EMPLOYER},
+                currentUserAccountIDParam: ESH_ACCOUNT_ID,
+                currentUserEmailParam: ESH_EMAIL,
+                isSelfTourViewed: false,
+                betas: undefined,
+                hasActiveAdminPolicies: false,
+                activePolicy: undefined,
+                type: CONST.POLICY.TYPE.SUBMIT,
+            });
+            await waitForBatchedUpdates();
+
+            await TestHelper.getOnyxData({
+                key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+                waitForCollectionCallback: false,
+                callback: (policy) => {
+                    expect(policy?.type).toBe(CONST.POLICY.TYPE.SUBMIT);
+                    expect(policy?.role).toBe(CONST.POLICY.ROLE.EDITOR);
+                    expect(policy?.approvalMode).toBe(CONST.POLICY.APPROVAL_MODE.ADVANCED);
+                    expect(policy?.areWorkflowsEnabled).toBe(true);
+                    expect(policy?.areTagsEnabled).toBe(true);
+                    expect(policy?.areDistanceRatesEnabled).toBe(true);
+                    expect(policy?.areCompanyCardsEnabled).toBe(false);
+                    expect(policy?.harvesting?.enabled).toBe(false);
+                    expect(policy?.autoReportingFrequency).toBe(CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE);
+                    expect(policy?.employeeList?.[ESH_EMAIL]?.role).toBe(CONST.POLICY.ROLE.EDITOR);
+                },
+            });
+        });
+
         it('should pass areDistanceRatesEnabled as true when creating workspace with distance rates feature enabled', async () => {
             await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID});
             await waitForBatchedUpdates();
diff --git a/tests/ui/OnboardingPurpose.tsx b/tests/ui/OnboardingPurpose.tsx
index 1b48f6e54cce..5a487f15f185 100644
--- a/tests/ui/OnboardingPurpose.tsx
+++ b/tests/ui/OnboardingPurpose.tsx
@@ -13,6 +13,7 @@ import Navigation from '@libs/Navigation/Navigation';
 import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
 import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types';
 import OnboardingPurpose from '@pages/OnboardingPurpose';
+import {createWorkspace} from '@userActions/Policy/Policy';
 import {completeOnboarding} from '@userActions/Report';
 import CONST from '@src/CONST';
 import IntlStore from '@src/languages/IntlStore';
@@ -24,6 +25,7 @@ import * as TestHelper from '../utils/TestHelper';
 import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
 
 const mockCompleteOnboarding = jest.mocked(completeOnboarding);
+const mockCreateWorkspace = jest.mocked(createWorkspace);
 
 jest.mock('@userActions/Report', () => {
     // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -35,6 +37,19 @@ jest.mock('@userActions/Report', () => {
     };
 });
 
+jest.mock('@userActions/Policy/Policy', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@userActions/Policy/Policy');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        createWorkspace: jest.fn().mockReturnValue({
+            policyID: 'test-policy-id',
+            adminsChatReportID: 'test-admins-report-id',
+        }),
+    };
+});
+
 TestHelper.setupGlobalFetchMock();
 
 // Helper to translate onboarding purpose keys that use dynamic CONST values
@@ -138,6 +153,132 @@ describe('OnboardingPurpose Page', () => {
         await waitForBatchedUpdatesWithAct();
     });
 
+    it('should navigate to personal details page when user selects EMPLOYER with Submit2026 beta and is from public domain', async () => {
+        await TestHelper.signInWithTestUser();
+
+        await act(async () => {
+            await Onyx.merge(ONYXKEYS.ACCOUNT, {
+                isFromPublicDomain: true,
+                hasAccessibleDomainPolicies: false,
+            });
+            await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]);
+        });
+
+        const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''});
+
+        await waitForBatchedUpdatesWithAct();
+
+        const user = userEvent.setup();
+        const employerLabel = translatePurpose(CONST.ONBOARDING_CHOICES.EMPLOYER);
+        const employerOption = screen.getByLabelText(employerLabel);
+        await user.press(employerOption);
+
+        await waitFor(() => {
+            expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_PERSONAL_DETAILS.getRoute(''));
+        });
+
+        unmount();
+        await waitForBatchedUpdatesWithAct();
+    });
+
+    it('should navigate to workspaces page when user selects EMPLOYER with Submit2026 beta and is from private domain with name set', async () => {
+        const testEmail = 'test@user.com';
+        await TestHelper.signInWithTestUser();
+
+        await act(async () => {
+            await Onyx.merge(ONYXKEYS.ACCOUNT, {
+                isFromPublicDomain: false,
+                hasAccessibleDomainPolicies: true,
+            });
+            await Onyx.merge(ONYXKEYS.LOGIN_LIST, {
+                [testEmail]: {
+                    partnerName: 'expensify.com',
+                    partnerUserID: testEmail,
+                    validatedDate: 'fake-validatedDate',
+                },
+            });
+            await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]);
+            await Onyx.merge(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM, {
+                firstName: 'Test',
+                lastName: 'User',
+            });
+        });
+
+        const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''});
+
+        await waitForBatchedUpdatesWithAct();
+
+        const user = userEvent.setup();
+        const employerLabel = translatePurpose(CONST.ONBOARDING_CHOICES.EMPLOYER);
+        const employerOption = screen.getByLabelText(employerLabel);
+        await user.press(employerOption);
+
+        await waitFor(() => {
+            expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_WORKSPACES.getRoute(''));
+        });
+
+        unmount();
+        await waitForBatchedUpdatesWithAct();
+    });
+
+    it('should create a Submit workspace from Purpose when EMPLOYER is selected and personal details already exist', async () => {
+        jest.spyOn(Navigation, 'dismissModal').mockImplementation(() => {});
+        jest.spyOn(Navigation, 'setNavigationActionToMicrotaskQueue').mockImplementation((callback: () => void) => callback());
+
+        await TestHelper.signInWithTestUser();
+
+        await act(async () => {
+            await Onyx.merge(ONYXKEYS.ACCOUNT, {
+                isFromPublicDomain: true,
+                hasAccessibleDomainPolicies: false,
+            });
+            await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]);
+            await Onyx.merge(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM, {
+                firstName: 'Test',
+                lastName: 'User',
+            });
+        });
+
+        const onyxSetSpy = jest.spyOn(Onyx, 'set');
+        onyxSetSpy.mockClear();
+
+        const {unmount} = renderOnboardingPurposePage(SCREENS.ONBOARDING.PURPOSE, {backTo: ''});
+
+        await waitForBatchedUpdatesWithAct();
+
+        const user = userEvent.setup();
+        const employerLabel = translatePurpose(CONST.ONBOARDING_CHOICES.EMPLOYER);
+        const employerOption = screen.getByLabelText(employerLabel);
+        await user.press(employerOption);
+
+        await waitFor(() => {
+            expect(mockCreateWorkspace).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    type: CONST.POLICY.TYPE.SUBMIT,
+                    engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                }),
+            );
+        });
+
+        await waitFor(() => {
+            expect(mockCompleteOnboarding).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                    onboardingPolicyID: 'test-policy-id',
+                }),
+            );
+        });
+
+        await waitFor(() => {
+            expect(onyxSetSpy).toHaveBeenCalledWith(ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM);
+            expect(navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_CATEGORIES.getRoute('test-policy-id'));
+        });
+
+        onyxSetSpy.mockRestore();
+        unmount();
+        await waitForBatchedUpdatesWithAct();
+    });
+
     it('should call completeOnboarding with introSelected when user is from private domain and selects a direct-complete choice', async () => {
         await TestHelper.signInWithTestUser();
 
diff --git a/tests/ui/PersonalDetailsOnboarding.tsx b/tests/ui/PersonalDetailsOnboarding.tsx
index d165bcc96aaa..d7a7dde9d632 100644
--- a/tests/ui/PersonalDetailsOnboarding.tsx
+++ b/tests/ui/PersonalDetailsOnboarding.tsx
@@ -14,6 +14,8 @@ import Navigation from '@libs/Navigation/Navigation';
 import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
 import type {OnboardingModalNavigatorParamList} from '@navigation/types';
 import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails';
+import {createWorkspace} from '@userActions/Policy/Policy';
+import {completeOnboarding} from '@userActions/Report';
 import CONST from '@src/CONST';
 import IntlStore from '@src/languages/IntlStore';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -22,6 +24,32 @@ import SCREENS from '@src/SCREENS';
 import * as TestHelper from '../utils/TestHelper';
 import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
 
+const mockCreateWorkspace = jest.mocked(createWorkspace);
+const mockCompleteOnboarding = jest.mocked(completeOnboarding);
+
+jest.mock('@userActions/Report', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@userActions/Report');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        completeOnboarding: jest.fn(),
+    };
+});
+
+jest.mock('@userActions/Policy/Policy', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@userActions/Policy/Policy');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        createWorkspace: jest.fn().mockReturnValue({
+            policyID: 'test-policy-id',
+            adminsChatReportID: 'test-admins-report-id',
+        }),
+    };
+});
+
 TestHelper.setupGlobalFetchMock();
 
 const Stack = createPlatformStackNavigator<OnboardingModalNavigatorParamList>();
@@ -143,4 +171,84 @@ describe('OnboardingPersonalDetails Page', () => {
         unmount();
         await waitForBatchedUpdatesWithAct();
     });
+
+    it('should navigate to Onboarding workspaces page when submitting form with EMPLOYER + Submit2026 beta and validated private domain', async () => {
+        const testEmail = 'test@user.com';
+        await TestHelper.signInWithTestUser();
+
+        await act(async () => {
+            await Onyx.merge(ONYXKEYS.ACCOUNT, {
+                isFromPublicDomain: false,
+                hasAccessibleDomainPolicies: true,
+            });
+            await Onyx.merge(ONYXKEYS.LOGIN_LIST, {
+                [testEmail]: {
+                    partnerName: 'expensify.com',
+                    partnerUserID: testEmail,
+                    validatedDate: 'fake-validatedDate',
+                },
+            });
+            await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.EMPLOYER);
+            await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]);
+        });
+
+        const {unmount} = renderOnboardingPersonalDetailsPage(SCREENS.ONBOARDING.PERSONAL_DETAILS, {backTo: ''});
+
+        await waitForBatchedUpdatesWithAct();
+
+        fireEvent.press(screen.getByText(TestHelper.translateLocal('common.continue')));
+
+        await waitFor(() => {
+            expect(navigate).toHaveBeenCalledWith(ROUTES.ONBOARDING_WORKSPACES.getRoute());
+        });
+
+        unmount();
+        await waitForBatchedUpdatesWithAct();
+    });
+
+    it('should create a Submit workspace when submitting form with EMPLOYER + Submit2026 beta and public domain', async () => {
+        jest.spyOn(Navigation, 'dismissModal').mockImplementation(() => {});
+        jest.spyOn(Navigation, 'setNavigationActionToMicrotaskQueue').mockImplementation((callback: () => void) => callback());
+
+        await TestHelper.signInWithTestUser();
+
+        await act(async () => {
+            await Onyx.merge(ONYXKEYS.ACCOUNT, {
+                isFromPublicDomain: true,
+                hasAccessibleDomainPolicies: false,
+            });
+            await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.EMPLOYER);
+            await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]);
+        });
+
+        const {unmount} = renderOnboardingPersonalDetailsPage(SCREENS.ONBOARDING.PERSONAL_DETAILS, {backTo: ''});
+
+        await waitForBatchedUpdatesWithAct();
+
+        fireEvent.press(screen.getByText(TestHelper.translateLocal('common.continue')));
+
+        await waitFor(() => {
+            expect(mockCreateWorkspace).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    type: CONST.POLICY.TYPE.SUBMIT,
+                    engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                }),
+            );
+        });
+
+        await waitFor(() => {
+            expect(mockCompleteOnboarding).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                }),
+            );
+        });
+
+        await waitFor(() => {
+            expect(navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_CATEGORIES.getRoute('test-policy-id'));
+        });
+
+        unmount();
+        await waitForBatchedUpdatesWithAct();
+    });
 });
diff --git a/tests/ui/WorkspaceOnboarding.tsx b/tests/ui/WorkspaceOnboarding.tsx
index 134697814764..d2307caf85cd 100644
--- a/tests/ui/WorkspaceOnboarding.tsx
+++ b/tests/ui/WorkspaceOnboarding.tsx
@@ -13,13 +13,54 @@ import Navigation from '@libs/Navigation/Navigation';
 import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
 import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types';
 import OnboardingWorkspaces from '@pages/OnboardingWorkspaces';
+import {joinAccessiblePolicy} from '@userActions/Policy/Member';
+import {createWorkspace} from '@userActions/Policy/Policy';
+import {completeOnboarding} from '@userActions/Report';
 import CONST from '@src/CONST';
+import IntlStore from '@src/languages/IntlStore';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import SCREENS from '@src/SCREENS';
 import * as TestHelper from '../utils/TestHelper';
 import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
 
+const mockCreateWorkspace = jest.mocked(createWorkspace);
+const mockCompleteOnboarding = jest.mocked(completeOnboarding);
+const mockJoinAccessiblePolicy = jest.mocked(joinAccessiblePolicy);
+
+jest.mock('@userActions/Report', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@userActions/Report');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        completeOnboarding: jest.fn(),
+    };
+});
+
+jest.mock('@userActions/Policy/Policy', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@userActions/Policy/Policy');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        createWorkspace: jest.fn().mockReturnValue({
+            policyID: 'test-policy-id',
+            adminsChatReportID: 'test-admins-report-id',
+        }),
+    };
+});
+
+jest.mock('@userActions/Policy/Member', () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    const actual = jest.requireActual('@userActions/Policy/Member');
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+    return {
+        ...actual,
+        joinAccessiblePolicy: jest.fn(),
+    };
+});
+
 TestHelper.setupGlobalFetchMock();
 
 const Stack = createPlatformStackNavigator<OnboardingModalNavigatorParamList>();
@@ -49,6 +90,7 @@ describe('OnboardingWorkspaces Page', () => {
         Onyx.init({
             keys: ONYXKEYS,
         });
+        return IntlStore.load(CONST.LOCALES.EN);
     });
 
     beforeEach(() => {
@@ -173,4 +215,118 @@ describe('OnboardingWorkspaces Page', () => {
         unmount();
         await waitForBatchedUpdatesWithAct();
     });
+
+    it('should create a Submit workspace when skip is pressed with EMPLOYER purpose and Submit2026 beta', async () => {
+        jest.spyOn(Navigation, 'dismissModal').mockImplementation(() => {});
+        jest.spyOn(Navigation, 'setNavigationActionToMicrotaskQueue').mockImplementation((callback: () => void) => callback());
+
+        await TestHelper.signInWithTestUser();
+
+        await act(async () => {
+            await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {
+                hasCompletedGuidedSetupFlow: false,
+            });
+            await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.EMPLOYER);
+            await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]);
+        });
+
+        const {unmount} = renderOnboardingWorkspacesPage(SCREENS.ONBOARDING.WORKSPACES, {backTo: ''});
+
+        await waitForBatchedUpdatesWithAct();
+
+        const skipButton = screen.getByTestId('onboardingWorkSpaceSkipButton');
+
+        const mockEvent = {
+            nativeEvent: {},
+            type: 'press',
+            target: skipButton,
+            currentTarget: skipButton,
+        };
+
+        fireEvent.press(skipButton, mockEvent);
+
+        await waitFor(() => {
+            expect(mockCreateWorkspace).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    type: CONST.POLICY.TYPE.SUBMIT,
+                    engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                }),
+            );
+        });
+
+        await waitFor(() => {
+            expect(mockCompleteOnboarding).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                }),
+            );
+        });
+
+        await waitFor(() => {
+            expect(navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_CATEGORIES.getRoute('test-policy-id'));
+        });
+
+        unmount();
+        await waitForBatchedUpdatesWithAct();
+    });
+
+    it('should complete onboarding with the joined Submit workspace policyID and open Categories in the admins room', async () => {
+        jest.spyOn(Navigation, 'dismissModal').mockImplementation(() => {});
+        jest.spyOn(Navigation, 'setNavigationActionToMicrotaskQueue').mockImplementation((callback: () => void) => callback());
+
+        await TestHelper.signInWithTestUser();
+
+        await act(async () => {
+            await Onyx.merge(ONYXKEYS.NVP_ONBOARDING, {
+                hasCompletedGuidedSetupFlow: false,
+            });
+            await Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.EMPLOYER);
+            await Onyx.set(ONYXKEYS.BETAS, [CONST.BETAS.SUBMIT_2026]);
+            await Onyx.merge(ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM, {
+                firstName: 'Test',
+                lastName: 'User',
+            });
+            await Onyx.set(ONYXKEYS.JOINABLE_POLICIES, {
+                submitPolicyID: {
+                    policyID: 'submit-policy-id',
+                    policyName: 'Submit Workspace',
+                    policyOwner: 'owner@test.com',
+                    employeeCount: 4,
+                    hasPendingAccess: false,
+                    automaticJoiningEnabled: true,
+                },
+            });
+        });
+
+        const onyxSetSpy = jest.spyOn(Onyx, 'set');
+        onyxSetSpy.mockClear();
+
+        const {unmount} = renderOnboardingWorkspacesPage(SCREENS.ONBOARDING.WORKSPACES, {backTo: ''});
+
+        await waitForBatchedUpdatesWithAct();
+
+        fireEvent.press(screen.getByText(TestHelper.translateLocal('workspace.workspaceList.joinNow')));
+
+        await waitFor(() => {
+            expect(mockJoinAccessiblePolicy).toHaveBeenCalledWith('submit-policy-id');
+        });
+
+        await waitFor(() => {
+            expect(mockCompleteOnboarding).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                    onboardingPolicyID: 'submit-policy-id',
+                }),
+            );
+        });
+
+        await waitFor(() => {
+            expect(onyxSetSpy).toHaveBeenCalledWith(ONYXKEYS.NVP_ONBOARDING_RHP_VARIANT, CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM);
+            expect(navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_CATEGORIES.getRoute('submit-policy-id'));
+        });
+
+        onyxSetSpy.mockRestore();
+        unmount();
+        await waitForBatchedUpdatesWithAct();
+    });
 });
diff --git a/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts
new file mode 100644
index 000000000000..a01b7002d801
--- /dev/null
+++ b/tests/unit/hooks/useAutoCreateSubmitWorkspace.test.ts
@@ -0,0 +1,330 @@
+import {renderHook} from '@testing-library/react-native';
+import useAutoCreateSubmitWorkspace from '@hooks/useAutoCreateSubmitWorkspace';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useHasActiveAdminPolicies from '@hooks/useHasActiveAdminPolicies';
+import useLocalize from '@hooks/useLocalize';
+import useOnboardingMessages from '@hooks/useOnboardingMessages';
+import useOnyx from '@hooks/useOnyx';
+import usePreferredPolicy from '@hooks/usePreferredPolicy';
+import * as navigateAfterOnboarding from '@libs/navigateAfterOnboarding';
+import * as Policy from '@userActions/Policy/Policy';
+import * as Report from '@userActions/Report';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
+
+jest.mock('@hooks/useOnyx', () => {
+    return {__esModule: true, default: jest.fn(() => [undefined])};
+});
+
+jest.mock('@hooks/useCurrentUserPersonalDetails');
+jest.mock('@hooks/useHasActiveAdminPolicies');
+jest.mock('@hooks/useLocalize');
+jest.mock('@hooks/usePreferredPolicy');
+jest.mock('@hooks/useOnboardingMessages');
+
+const mockTranslate = jest.fn((key: string) => key);
+const mockFormatPhoneNumber = jest.fn((phone: string) => phone);
+
+const MOCK_SESSION = {
+    accountID: 12345,
+    email: 'test@expensify.com',
+};
+
+const MOCK_POLICY_ID = 'mock-policy-id';
+const MOCK_ADMINS_CHAT_REPORT_ID = 'mock-admins-chat-report-id';
+const MOCK_ONBOARDING_MESSAGE = {message: 'Welcome!', video: undefined, tasks: []};
+
+function setupDefaultMocks() {
+    const mockUseOnyx = useOnyx as jest.Mock;
+    mockUseOnyx.mockImplementation((key: string) => {
+        if (key === 'session') {
+            return [MOCK_SESSION];
+        }
+        if (key === 'betas') {
+            return [[]];
+        }
+        if (key.startsWith('policy_')) {
+            return [false];
+        }
+        return [undefined];
+    });
+
+    (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({
+        accountID: MOCK_SESSION.accountID,
+        login: MOCK_SESSION.email,
+        localCurrencyCode: 'USD',
+    });
+
+    (useLocalize as jest.Mock).mockReturnValue({
+        translate: mockTranslate,
+        formatPhoneNumber: mockFormatPhoneNumber,
+    });
+
+    (usePreferredPolicy as jest.Mock).mockReturnValue({
+        isRestrictedToPreferredPolicy: false,
+        preferredPolicyID: undefined,
+        isRestrictedPolicyCreation: false,
+    });
+
+    (useHasActiveAdminPolicies as jest.Mock).mockReturnValue(false);
+
+    (useOnboardingMessages as jest.Mock).mockReturnValue({
+        onboardingMessages: {
+            [CONST.ONBOARDING_CHOICES.EMPLOYER]: MOCK_ONBOARDING_MESSAGE,
+        },
+    });
+}
+
+describe('useAutoCreateSubmitWorkspace', () => {
+    const createWorkspaceSpy = jest.spyOn(Policy, 'createWorkspace').mockReturnValue({
+        policyID: MOCK_POLICY_ID,
+        adminsChatReportID: MOCK_ADMINS_CHAT_REPORT_ID,
+    } as ReturnType<typeof Policy.createWorkspace>);
+    const completeOnboardingSpy = jest.spyOn(Report, 'completeOnboarding').mockImplementation(jest.fn());
+    const setOnboardingAdminsChatReportIDSpy = jest.spyOn(Welcome, 'setOnboardingAdminsChatReportID').mockImplementation(jest.fn());
+    const setOnboardingPolicyIDSpy = jest.spyOn(Welcome, 'setOnboardingPolicyID').mockImplementation(jest.fn());
+    const navigateSpy = jest.spyOn(navigateAfterOnboarding, 'navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue').mockImplementation(jest.fn());
+
+    beforeEach(() => {
+        jest.clearAllMocks();
+        setupDefaultMocks();
+    });
+
+    it('creates a Submit workspace with the correct parameters for a new EMPLOYER user', () => {
+        // Given a new user going through onboarding with no existing workspace (onboardingPolicyID is undefined,
+        // hasEditableGroupPolicy is false, and policy creation is not restricted)
+
+        // When the autoCreateSubmitWorkspace function is invoked during onboarding
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('John', 'Doe');
+
+        // Then a Submit workspace should be created because EMPLOYER users without an existing
+        // workspace need one auto-created to land on after onboarding
+        expect(createWorkspaceSpy).toHaveBeenCalledTimes(1);
+        expect(createWorkspaceSpy).toHaveBeenCalledWith(
+            expect.objectContaining({
+                makeMeAdmin: true,
+                engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                currency: 'USD',
+                shouldAddOnboardingTasks: false,
+                shouldAddGuideWelcomeMessage: false,
+                type: CONST.POLICY.TYPE.SUBMIT,
+                currentUserAccountIDParam: MOCK_SESSION.accountID,
+                currentUserEmailParam: MOCK_SESSION.email,
+            }),
+        );
+    });
+
+    it('completes onboarding with the newly created workspace and admins chat IDs', () => {
+        // Given a new user with no pre-existing onboarding workspace
+
+        // When the hook creates a workspace and finishes the onboarding flow
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('John', 'Doe');
+
+        // Then completeOnboarding should receive the IDs returned by createWorkspace so the backend
+        // can associate the guided setup data with the correct workspace and admins chat
+        expect(completeOnboardingSpy).toHaveBeenCalledTimes(1);
+        expect(completeOnboardingSpy).toHaveBeenCalledWith(
+            expect.objectContaining({
+                engagementChoice: CONST.ONBOARDING_CHOICES.EMPLOYER,
+                onboardingMessage: MOCK_ONBOARDING_MESSAGE,
+                firstName: 'John',
+                lastName: 'Doe',
+                adminsChatReportID: MOCK_ADMINS_CHAT_REPORT_ID,
+                onboardingPolicyID: MOCK_POLICY_ID,
+            }),
+        );
+    });
+
+    it('clears onboarding state after the flow completes', () => {
+        // Given a user completing the onboarding flow
+
+        // When autoCreateSubmitWorkspace finishes
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('John', 'Doe');
+
+        // Then the transient onboarding Onyx keys should be cleared so the onboarding
+        // flow is not re-triggered on subsequent app launches
+        expect(setOnboardingAdminsChatReportIDSpy).toHaveBeenCalledTimes(1);
+        expect(setOnboardingPolicyIDSpy).toHaveBeenCalledTimes(1);
+    });
+
+    it('navigates to the submit workspace page after completing onboarding', () => {
+        // Given a user completing the EMPLOYER onboarding flow
+
+        // When autoCreateSubmitWorkspace finishes setting up the workspace
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('John', 'Doe');
+
+        // Then the user should be navigated to the newly created Submit workspace
+        // so they land on their workspace immediately after onboarding
+        expect(navigateSpy).toHaveBeenCalledTimes(1);
+        expect(navigateSpy).toHaveBeenCalledWith(MOCK_POLICY_ID, expect.any(Boolean));
+    });
+
+    it('reuses the existing onboarding workspace instead of creating a new one', () => {
+        // Given a user who already has an onboardingPolicyID set (e.g. assigned by an admin
+        // or from a previous partial onboarding attempt)
+        const existingPolicyID = 'existing-policy-id';
+        const existingAdminsReportID = 'existing-admins-report-id';
+
+        (useOnyx as jest.Mock).mockImplementation((key: string) => {
+            if (key === 'onboardingPolicyID') {
+                return [existingPolicyID];
+            }
+            if (key === 'onboardingAdminsChatReportID') {
+                return [existingAdminsReportID];
+            }
+            if (key === 'session') {
+                return [MOCK_SESSION];
+            }
+            if (key === 'betas') {
+                return [[]];
+            }
+            if (key.startsWith('policy_')) {
+                return [false];
+            }
+            return [undefined];
+        });
+
+        // When the onboarding flow runs
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('Jane', 'Smith');
+
+        // Then no new workspace should be created, and completeOnboarding should use
+        // the pre-existing IDs to avoid creating duplicate workspaces
+        expect(createWorkspaceSpy).not.toHaveBeenCalled();
+        expect(completeOnboardingSpy).toHaveBeenCalledWith(
+            expect.objectContaining({
+                adminsChatReportID: existingAdminsReportID,
+                onboardingPolicyID: existingPolicyID,
+            }),
+        );
+    });
+
+    it('skips workspace creation when the user is already a paid group policy admin', () => {
+        // Given a user who is already an admin of a paid group policy
+        (useOnyx as jest.Mock).mockImplementation((key: string) => {
+            if (key === 'session') {
+                return [MOCK_SESSION];
+            }
+            if (key === 'betas') {
+                return [[]];
+            }
+            if (key === 'onboardingPolicyID') {
+                return [undefined];
+            }
+            if (key === 'onboardingAdminsChatReportID') {
+                return [undefined];
+            }
+            if (key.startsWith('policy_')) {
+                return [true];
+            }
+            return [undefined];
+        });
+
+        // When onboarding completes
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('Jane', 'Smith');
+
+        // Then no Submit workspace should be created because the user already has
+        // a paid group workspace and creating another would be redundant
+        expect(createWorkspaceSpy).not.toHaveBeenCalled();
+    });
+
+    it('skips workspace creation when the user domain restricts policy creation', () => {
+        // Given a user whose domain security group has enableRestrictedPolicyCreation set to true
+        (usePreferredPolicy as jest.Mock).mockReturnValue({
+            isRestrictedToPreferredPolicy: false,
+            preferredPolicyID: undefined,
+            isRestrictedPolicyCreation: true,
+        });
+
+        // When the onboarding flow runs
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('Jane', 'Smith');
+
+        // Then workspace creation should be skipped because the domain admin has
+        // restricted users from creating their own policies
+        expect(createWorkspaceSpy).not.toHaveBeenCalled();
+        expect(completeOnboardingSpy).toHaveBeenCalledTimes(1);
+    });
+
+    it('still completes onboarding and navigates even when workspace creation is skipped', () => {
+        // Given a user who cannot create a workspace due to domain restrictions
+        (usePreferredPolicy as jest.Mock).mockReturnValue({
+            isRestrictedToPreferredPolicy: false,
+            preferredPolicyID: undefined,
+            isRestrictedPolicyCreation: true,
+        });
+
+        // When the onboarding flow runs
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('Jane', 'Smith');
+
+        // Then onboarding should still be completed and navigation should still occur
+        // because the user needs to finish onboarding regardless of workspace creation
+        expect(completeOnboardingSpy).toHaveBeenCalledTimes(1);
+        expect(setOnboardingAdminsChatReportIDSpy).toHaveBeenCalledTimes(1);
+        expect(setOnboardingPolicyIDSpy).toHaveBeenCalledTimes(1);
+        expect(navigateSpy).toHaveBeenCalledTimes(1);
+    });
+
+    it('uses the localCurrencyCode from personal details for workspace currency', () => {
+        // Given a user whose personal details have localCurrencyCode set to GBP
+        (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({
+            accountID: MOCK_SESSION.accountID,
+            login: MOCK_SESSION.email,
+            localCurrencyCode: 'GBP',
+        });
+
+        // When a workspace is created during onboarding
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('John', 'Doe');
+
+        // Then the workspace should use GBP as its currency so it matches the user's locale
+        expect(createWorkspaceSpy).toHaveBeenCalledWith(
+            expect.objectContaining({
+                currency: 'GBP',
+            }),
+        );
+    });
+
+    it('falls back to USD when localCurrencyCode is not available', () => {
+        // Given a user whose personal details do not have a localCurrencyCode set
+        (useCurrentUserPersonalDetails as jest.Mock).mockReturnValue({
+            accountID: MOCK_SESSION.accountID,
+            login: MOCK_SESSION.email,
+            localCurrencyCode: undefined,
+        });
+
+        // When a workspace is created during onboarding
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('John', 'Doe');
+
+        // Then the workspace should default to USD as a safe fallback currency
+        expect(createWorkspaceSpy).toHaveBeenCalledWith(
+            expect.objectContaining({
+                currency: CONST.CURRENCY.USD,
+            }),
+        );
+    });
+
+    it('forwards firstName and lastName to completeOnboarding for display name setup', () => {
+        // Given a user providing their name during the onboarding personal details step
+
+        // When the onboarding flow completes
+        const {result} = renderHook(() => useAutoCreateSubmitWorkspace());
+        result.current('Alice', 'Wonderland');
+
+        // Then the provided name should be passed through to completeOnboarding so the
+        // backend can set up the user's display name as part of the guided setup
+        expect(completeOnboardingSpy).toHaveBeenCalledWith(
+            expect.objectContaining({
+                firstName: 'Alice',
+                lastName: 'Wonderland',
+            }),
+        );
+    });
+});
