diff --git a/src/ROUTES.js b/src/ROUTES.js
index 8af98287e5d8..e27b24cc61b7 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -49,6 +49,8 @@ export default {
     VALIDATE_LOGIN_WITH_VALIDATE_CODE: 'v/:accountID/:validateCode',
     ENABLE_PAYMENTS: 'enable-payments',
     WORKSPACE_NEW: 'workspace/new',
+    WORKSPACE_INVITE: 'workspace/:policyID/invite',
+    getWorkspaceInviteRoute: policyID => `workspace/${policyID}/invite`,
 
     /**
      * @param {String} route
diff --git a/src/languages/en.js b/src/languages/en.js
index fea831a19833..a6667626d63b 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -35,6 +35,7 @@ export default {
         saveAndContinue: 'Save & Continue',
         settings: 'Settings',
         termsOfService: 'Terms of Service',
+        invite: 'Invite',
     },
     attachmentPicker: {
         cameraPermissionRequired: 'Camera Permission Required',
@@ -347,5 +348,14 @@ export default {
             uploadPhoto: 'Upload Photo',
             requestCall: 'Request a call',
         },
+        invite: {
+            invitePeople: 'Invite People',
+            invitePeoplePrompt: 'Invite a colleague to your workspace.',
+            personalMessagePrompt: 'Add a Personal Message (Optional)',
+            enterEmailOrPhone: 'Email or Phone',
+            pleaseEnterValidLogin: 'Please ensure the email or phone number is valid (e.g. +15005550006).',
+            genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.',
+            welcomeNote: ({workspaceName}) => `You have been invited to the ${workspaceName} Workspace! Download the Expensify mobile App to start tracking your expenses.`,
+        },
     },
 };
diff --git a/src/languages/es.js b/src/languages/es.js
index 00e53087b9d2..2aae71d0c220 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -33,6 +33,7 @@ export default {
         saveAndContinue: 'Guardar y Continuar',
         settings: 'Configuración',
         termsOfService: 'Términos de servicio',
+        invite: 'Invitar',
     },
     attachmentPicker: {
         cameraPermissionRequired: 'Se necesita permiso para usar la cámara',
@@ -301,5 +302,14 @@ export default {
             editPhoto: 'Editar Foto',
             requestCall: 'Concertar una llamada',
         },
+        invite: {
+            invitePeople: 'Invitar a la gente',
+            invitePeoplePrompt: 'Invita a un colega a tu espacio de trabajo.',
+            personalMessagePrompt: 'Agregar un mensaje personal (Opcional)',
+            enterEmailOrPhone: 'Email o teléfono',
+            pleaseEnterValidLogin: 'Asegúrese de que el correo electrónico o el número de teléfono sean válidos (e.g. +15005550006).',
+            genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..',
+            welcomeNote: ({workspaceName}) => `¡Has sido invitado a la ${workspaceName} Espacio de trabajo! Descargue la aplicación móvil Expensify para comenzar a rastrear sus gastos.`,
+        },
     },
 };
diff --git a/src/libs/API.js b/src/libs/API.js
index b5a76e0acd67..4a7e93120196 100644
--- a/src/libs/API.js
+++ b/src/libs/API.js
@@ -406,6 +406,17 @@ function GetIOUReport(parameters) {
     return Network.post(commandName, parameters);
 }
 
+/**
+ * @returns {Promise}
+ */
+function GetPolicyList() {
+    const commandName = 'Get';
+    const parameters = {
+        returnValueList: 'policyList',
+    };
+    return Network.post(commandName, parameters);
+}
+
 /**
  * @returns {Promise}
  */
@@ -798,6 +809,21 @@ function BankAccount_Get(parameters) {
     return Network.post(commandName, parameters, CONST.NETWORK.METHOD.POST, true);
 }
 
+/**
+ * @param {Object} parameters
+ * @param {Object[]} parameters.employees
+ * @param {String} parameters.welcomeNote
+ * @param {String} parameters.policyID
+ * @returns {Promise}
+ */
+function Policy_Employees_Merge(parameters) {
+    const commandName = 'Policy_Employees_Merge';
+    requireParameters(['employees', 'welcomeNote', 'policyID'], parameters, commandName);
+
+    // Always include returnPersonalDetails to ensure we get the employee's personal details in the response
+    return Network.post(commandName, {...parameters, returnPersonalDetails: true});
+}
+
 /**
  * @param {Object} parameters
  * @param {String} parameters.accountNumber
@@ -915,6 +941,7 @@ export {
     Get,
     GetAccountStatus,
     GetIOUReport,
+    GetPolicyList,
     GetPolicySummaryList,
     GetRequestCountryCode,
     Graphite_Timer,
@@ -924,6 +951,7 @@ export {
     PersonalDetails_GetForEmails,
     PersonalDetails_Update,
     Plaid_GetLinkToken,
+    Policy_Employees_Merge,
     Push_Authenticate,
     RejectTransaction,
     Report_AddComment,
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index f3cd8f074df1..dd51541077d5 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -27,7 +27,7 @@ import Navigation from '../Navigation';
 import * as User from '../../actions/User';
 import {setModalVisibility} from '../../actions/Modal';
 import NameValuePair from '../../actions/NameValuePair';
-import {getPolicySummaries} from '../../actions/Policy';
+import {getPolicySummaries, getPolicyList} from '../../actions/Policy';
 import modalCardStyleInterpolator from './modalCardStyleInterpolator';
 import createCustomModalStackNavigator from './createCustomModalStackNavigator';
 
@@ -52,6 +52,7 @@ import {
     AddPersonalBankAccountModalStackNavigator,
     ReimbursementAccountModalStackNavigator,
     NewWorkspaceStackNavigator,
+    WorkspaceInviteModalStackNavigator,
 } from './ModalStackNavigators';
 import SCREENS from '../../../SCREENS';
 import Timers from '../../Timers';
@@ -128,6 +129,7 @@ class AuthScreens extends React.Component {
         fetchCountryCodeByRequestIP();
         UnreadIndicatorUpdater.listenForReportChanges();
         getPolicySummaries();
+        getPolicyList();
 
         // Refresh the personal details, timezone and betas every 30 minutes
         // There is no pusher event that sends updated personal details data yet
@@ -290,6 +292,12 @@ class AuthScreens extends React.Component {
                     component={ReimbursementAccountModalStackNavigator}
                     listeners={modalScreenListeners}
                 />
+                <RootStack.Screen
+                    name="WorkspaceInvite"
+                    options={modalScreenOptions}
+                    component={WorkspaceInviteModalStackNavigator}
+                    listeners={modalScreenListeners}
+                />
             </RootStack.Navigator>
         );
     }
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index 90c941ec2f1f..2c48f3785e07 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -21,6 +21,7 @@ import IOUCurrencySelection from '../../../pages/iou/IOUCurrencySelection';
 import ReportParticipantsPage from '../../../pages/ReportParticipantsPage';
 import EnablePaymentsPage from '../../../pages/EnablePayments';
 import AddPersonalBankAccountPage from '../../../pages/AddPersonalBankAccountPage';
+import WorkspaceInvitePage from '../../../pages/workspace/WorkspaceInvitePage';
 import ReimbursementAccountPage from '../../../pages/ReimbursementAccount/ReimbursementAccountPage';
 import NewWorkspacePage from '../../../pages/workspace/NewWorkspacePage';
 
@@ -164,6 +165,11 @@ const NewWorkspaceStackNavigator = createModalStackNavigator([{
     name: 'NewWorkspace_Root',
 }]);
 
+const WorkspaceInviteModalStackNavigator = createModalStackNavigator([{
+    Component: WorkspaceInvitePage,
+    name: 'WorkspaceInvite_Root',
+}]);
+
 export {
     IOUBillStackNavigator,
     IOURequestModalStackNavigator,
@@ -178,4 +184,5 @@ export {
     AddPersonalBankAccountModalStackNavigator,
     ReimbursementAccountModalStackNavigator,
     NewWorkspaceStackNavigator,
+    WorkspaceInviteModalStackNavigator,
 };
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index e18b0b75ae1c..03533590b820 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -119,6 +119,11 @@ export default {
                     ReimbursementAccount_Root: ROUTES.ADD_VERIFIED_BANK_ACCOUNT,
                 },
             },
+            WorkspaceInvite: {
+                screens: {
+                    WorkspaceInvite_Root: ROUTES.WORKSPACE_INVITE,
+                },
+            },
             NewWorkspace: {
                 screens: {
                     NewWorkspace_Root: ROUTES.WORKSPACE_NEW,
diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js
index fff00ea6a2d7..fac49bf4fd60 100644
--- a/src/libs/actions/PersonalDetails.js
+++ b/src/libs/actions/PersonalDetails.js
@@ -303,6 +303,7 @@ NetworkConnection.onReconnect(fetchPersonalDetails);
 
 export {
     fetchPersonalDetails,
+    formatPersonalDetails,
     getFromReportParticipants,
     getDisplayName,
     getDefaultAvatar,
diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js
index af04011c3c94..15867a448c5f 100644
--- a/src/libs/actions/Policy.js
+++ b/src/libs/actions/Policy.js
@@ -1,7 +1,31 @@
 import _ from 'underscore';
 import Onyx from 'react-native-onyx';
-import {GetPolicySummaryList} from '../API';
+import {GetPolicySummaryList, GetPolicyList, Policy_Employees_Merge} from '../API';
 import ONYXKEYS from '../../ONYXKEYS';
+import {formatPersonalDetails} from './PersonalDetails';
+import Growl from '../Growl';
+import CONST from '../../CONST';
+import {translate} from '../translate';
+
+const allPolicies = {};
+Onyx.connect({
+    key: ONYXKEYS.COLLECTION.POLICY,
+    callback: (val, key) => {
+        if (val && key) {
+            allPolicies[key] = {...allPolicies[key], ...val};
+        }
+    },
+});
+
+let translateLocal = (phrase, variables) => translate(CONST.DEFAULT_LOCALE, phrase, variables);
+Onyx.connect({
+    key: ONYXKEYS.PREFERRED_LOCALE,
+    callback: (preferredLocale) => {
+        if (preferredLocale) {
+            translateLocal = (phrase, variables) => translate(preferredLocale, phrase, variables);
+        }
+    },
+});
 
 /**
  * Takes a full policy summary that is returned from the policySummaryList and simplifies it so we are only storing
@@ -17,6 +41,27 @@ function getSimplifiedPolicyObject(fullPolicy) {
     };
 }
 
+/**
+ * Simplifies the policyList response into an object containing an array of emails
+ *
+ * @param {Object} fullPolicy
+ * @returns {Object}
+ */
+function getSimplifiedEmployeeListObject(fullPolicy) {
+    const employeeListEmails = _.chain(fullPolicy.value.employeeList)
+        .pluck('email')
+        .flatten()
+        .unique()
+        .value();
+
+    return {
+        employeeList: employeeListEmails,
+    };
+}
+
+/**
+ * Fetches the policySummaryList from the API and saves a simplified version in Onyx
+ */
 function getPolicySummaries() {
     GetPolicySummaryList()
         .then((data) => {
@@ -30,7 +75,69 @@ function getPolicySummaries() {
         });
 }
 
+/**
+ * Fetches the policyList from the API and saves a simplified version in Onyx
+ */
+function getPolicyList() {
+    GetPolicyList()
+        .then((data) => {
+            if (data.jsonCode === 200) {
+                const policyDataToStore = _.reduce(data.policyList, (memo, policy) => ({
+                    ...memo,
+                    [`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: getSimplifiedEmployeeListObject(policy),
+                }), {});
+                Onyx.mergeCollection(ONYXKEYS.COLLECTION.POLICY, policyDataToStore);
+            }
+        });
+}
+
+/**
+ * Merges the passed in login into the specified policy
+ *
+ * @param {String} login
+ * @param {String} welcomeNote
+ * @param {String} policyID
+ */
+function invite(login, welcomeNote, policyID) {
+    const key = `${ONYXKEYS.COLLECTION.POLICY}${policyID}`;
+
+    // Make a shallow copy to preserve original data, and concat the login
+    const policy = _.clone(allPolicies[key]);
+    policy.employeeList = [...policy.employeeList, login];
+
+    // Optimistically add the user to the policy
+    Onyx.set(key, policy);
+
+    // Make the API call to merge the login into the policy
+    Policy_Employees_Merge({
+        employees: JSON.stringify([{email: login}]),
+        welcomeNote,
+        policyID,
+    })
+        .then((data) => {
+            // Save the personalDetails for the invited user in Onyx
+            if (data.jsonCode === 200) {
+                Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formatPersonalDetails(data.personalDetails));
+                return;
+            }
+
+            // If the operation failed, undo the optimistic addition
+            const policyDataWithoutLogin = _.clone(allPolicies[key]);
+            policyDataWithoutLogin.employeeList = _.without(allPolicies[key].employeeList, login);
+            Onyx.set(key, policyDataWithoutLogin);
+
+            // Show the user feedback that the addition failed
+            let errorMessage = translateLocal('workspace.invite.genericFailureMessage');
+            if (data.jsonCode === 402) {
+                errorMessage += ` ${translateLocal('workspace.invite.pleaseEnterValidLogin')}`;
+            }
+
+            Growl.show(errorMessage, CONST.GROWL.ERROR, 5000);
+        });
+}
+
 export {
-    // eslint-disable-next-line import/prefer-default-export
     getPolicySummaries,
+    getPolicyList,
+    invite,
 };
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
new file mode 100644
index 000000000000..7637ee9947ea
--- /dev/null
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -0,0 +1,156 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {TextInput, View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import Str from 'expensify-common/lib/str';
+import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import ScreenWrapper from '../../components/ScreenWrapper';
+import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
+import Navigation from '../../libs/Navigation/Navigation';
+import styles from '../../styles/styles';
+import Text from '../../components/Text';
+import Button from '../../components/Button';
+import compose from '../../libs/compose';
+import ONYXKEYS from '../../ONYXKEYS';
+import {invite} from '../../libs/actions/Policy';
+import TextLink from '../../components/TextLink';
+import getEmailKeyboardType from '../../libs/getEmailKeyboardType';
+import themeColors from '../../styles/themes/default';
+import Growl from '../../libs/Growl';
+import CONST from '../../CONST';
+
+const propTypes = {
+    ...withLocalizePropTypes,
+
+    /** The policy passed via the route */
+    policy: PropTypes.shape({
+        /** The policy name */
+        name: PropTypes.string,
+    }),
+
+    /** URL Route params */
+    route: PropTypes.shape({
+        /** Params from the URL path */
+        params: PropTypes.shape({
+            /** policyID passed via route: /workspace/:policyID/invite */
+            policyID: PropTypes.string,
+        }),
+    }).isRequired,
+};
+
+const defaultProps = {
+    policy: {
+        name: '',
+    },
+};
+
+class WorkspaceInvitePage extends React.Component {
+    constructor(props) {
+        super(props);
+
+        this.state = {
+            emailOrPhone: '',
+            welcomeNote: '',
+        };
+
+        this.inviteUser = this.inviteUser.bind(this);
+    }
+
+    /**
+     * Gets the welcome note default text
+     *
+     * @returns {Object}
+     */
+    getWelcomeNotePlaceholder() {
+        return this.props.translate('workspace.invite.welcomeNote', {
+            workspaceName: this.props.policy.name,
+        });
+    }
+
+    /**
+     * Handle the invite button click
+     */
+    inviteUser() {
+        if (!Str.isValidEmail(this.state.emailOrPhone) && !Str.isValidPhone(this.state.emailOrPhone)) {
+            Growl.show(this.props.translate('workspace.invite.pleaseEnterValidLogin'), CONST.GROWL.ERROR, 5000);
+            return;
+        }
+
+        invite(this.state.emailOrPhone, this.state.welcomeNote || this.getWelcomeNotePlaceholder(),
+            this.props.route.params.policyID);
+        Navigation.goBack();
+    }
+
+    render() {
+        return (
+            <ScreenWrapper>
+                <HeaderWithCloseButton
+                    title={this.props.translate('workspace.invite.invitePeople')}
+                    onCloseButtonPress={Navigation.dismissModal}
+                />
+                <View style={[styles.p5, styles.flex1, styles.overflowAuto]}>
+                    <View style={styles.flexGrow1}>
+                        <Text style={[styles.mb6, styles.textP]}>
+                            {this.props.translate('workspace.invite.invitePeoplePrompt')}
+                        </Text>
+                        <View style={styles.mb6}>
+                            <Text style={[styles.textP, styles.mb2]}>
+                                {this.props.translate('workspace.invite.enterEmailOrPhone')}
+                            </Text>
+                            <TextInput
+                                autoCompleteType="email"
+                                autoCorrect={false}
+                                autoCapitalize="none"
+                                style={[styles.textInput]}
+                                value={this.state.emailOrPhone}
+                                keyboardType={getEmailKeyboardType()}
+                                onChangeText={text => this.setState({emailOrPhone: text})}
+                            />
+                        </View>
+                        <View style={styles.mb6}>
+                            <Text style={[styles.textP, styles.mb2]}>
+                                {this.props.translate('workspace.invite.personalMessagePrompt')}
+                            </Text>
+                            <TextInput
+                                autoCompleteType="off"
+                                autoCorrect={false}
+                                style={[styles.textInput, styles.workspaceInviteWelcome, styles.mb6]}
+                                numberOfLines={10}
+                                textAlignVertical="top"
+                                multiline
+                                value={this.state.welcomeNote}
+                                placeholder={this.getWelcomeNotePlaceholder()}
+                                placeholderTextColor={themeColors.placeholderText}
+                                onChangeText={text => this.setState({welcomeNote: text})}
+                            />
+                            <TextLink href="https://use.expensify.com/privacy">
+                                {this.props.translate('common.privacy')}
+                            </TextLink>
+                        </View>
+                    </View>
+                    <View style={styles.flexGrow0}>
+                        <Button
+                            success
+                            style={[styles.mb2]}
+                            isDisabled={!this.state.emailOrPhone}
+                            text={this.props.translate('common.invite')}
+                            onPress={this.inviteUser}
+                        />
+                    </View>
+                </View>
+            </ScreenWrapper>
+        );
+    }
+}
+
+WorkspaceInvitePage.propTypes = propTypes;
+WorkspaceInvitePage.defaultProps = defaultProps;
+
+export default compose(
+    withLocalize,
+    withOnyx({
+        policy: {
+            key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`,
+        },
+    }),
+)(WorkspaceInvitePage);
diff --git a/src/styles/styles.js b/src/styles/styles.js
index daa8148bec93..8e5dccda69b6 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -1530,6 +1530,10 @@ const styles = {
     cursorDisabled: {
         cursor: 'not-allowed',
     },
+
+    workspaceInviteWelcome: {
+        minHeight: 150,
+    },
 };
 
 const baseCodeTagStyles = {
