Approval workflows define how expense reports are reviewed and approved in your organization. This guide explains how to configure and use approval workflows effectively.
Understanding Approval Workflows
Approval workflows determine who needs to approve expense reports before they can be reimbursed. New Expensify supports flexible workflows from simple to complex multi-level approvals.
Approval Modes
New Expensify offers several approval modes:
Optional
Basic
Advanced
Dynamic External
No approvals required. Reports are automatically approved upon submission.
Single-level approval. All expenses go to one approver or follow a simple rule.
Multi-level approval chains with custom workflows for different teams or expense types.
Integration with external approval systems using custom workflows.
Configuring Approval Workflows
Enabling Approvals
To enable approval workflows for your workspace:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
const optionItems : ToggleSettingOptionRowProps [] = useMemo (() => {
return [
{
title: translate ( 'workflowsPage.addApprovalsTitle' ),
subtitle: isSmartLimitEnabled ? translate ( 'workspace.moreFeatures.workflows.disableApprovalPrompt' ) : translate ( 'workflowsPage.addApprovalsDescription' ),
switchAccessibilityLabel: isSmartLimitEnabled ? translate ( 'workspace.moreFeatures.workflows.disableApprovalPrompt' ) : translate ( 'workflowsPage.addApprovalsDescription' ),
onToggle : ( isEnabled : boolean ) => {
if ( ! isEnabled ) {
showConfirmModal ({
title: translate ( 'workspace.bankAccount.areYouSure' ),
prompt: translate ( 'workflowsPage.disableApprovalPromptDescription' ),
confirmText: translate ( 'common.disable' ),
cancelText: translate ( 'common.cancel' ),
danger: true ,
}). then (( result ) => {
if ( result . action !== ModalActions . CONFIRM ) {
return ;
}
confirmDisableApprovals ();
});
return ;
}
setWorkspaceApprovalMode ( route . params . policyID , policy ?. owner ?? '' , isEnabled ? updateApprovalMode : CONST . POLICY . APPROVAL_MODE . OPTIONAL , {
reportNextSteps: allReportNextSteps ,
transactionViolations ,
betas ,
});
},
isActive: isDEWEnabled || (([ CONST . POLICY . APPROVAL_MODE . BASIC , CONST . POLICY . APPROVAL_MODE . ADVANCED ]. some (( approvalMode ) => approvalMode === policy ?. approvalMode ) && ! hasApprovalError ) ?? false ),
},
];
}, [ /* dependencies */ ]);
Setting Up Approval Workflows
Approval workflows can be configured with specific members and rules:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
const { approvalWorkflows , availableMembers , usedApproverEmails } = convertPolicyEmployeesToApprovalWorkflows ({
policy ,
personalDetails: personalDetails ?? {},
localeCompare ,
});
const isAdvanceApproval = ( approvalWorkflows . length > 1 || ( approvalWorkflows ?. at ( 0 )?. approvers ?? []). length > 1 ) && isControlPolicy ( policy );
const updateApprovalMode = isAdvanceApproval ? CONST . POLICY . APPROVAL_MODE . ADVANCED : CONST . POLICY . APPROVAL_MODE . BASIC ;
Managing Approval Workflows
Viewing Approval Workflows
The workflows page displays all configured approval workflows:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
<>
{ isDEWEnabled && (
< View style = { [ styles . border , shouldUseNarrowLayout ? styles . p3 : styles . p4 , styles . mt6 , styles . mbn3 , styles . flexRow , styles . alignItemsCenter ] } >
< Icon
src = { expensifyIcons . Info }
fill = { theme . textSupporting }
additionalStyles = { styles . popoverMenuIcon }
/>
< View style = { [ styles . flex1 , styles . ml3 ] } >
< RenderHTML
html = {
accountManagerReportID
? translate ( 'workflowsPage.customApprovalWorkflowEnabled' )
: translate ( 'workflowsPage.customApprovalWorkflowEnabledConciergeOnly' )
}
/>
</ View >
</ View >
) }
{ filteredApprovalWorkflows . length > CONST . APPROVAL_WORKFLOW_SEARCH_LIMIT && (
< SearchBar
label = { translate ( 'workflowsPage.findWorkflow' ) }
inputValue = { workflowSearchInput }
onChangeText = { setWorkflowSearchInput }
style = { [ styles . mt6 , { marginHorizontal: 0 }] }
/>
) }
{ searchFilteredWorkflows . map (( workflow ) => (
< OfflineWithFeedback
key = { workflow . approvers . at ( 0 )?. email }
pendingAction = { workflow . pendingAction }
>
< ApprovalWorkflowSection
approvalWorkflow = { workflow }
onPress = { () => Navigation . navigate ( ROUTES . WORKSPACE_WORKFLOWS_APPROVALS_EDIT . getRoute ( route . params . policyID , workflow . approvers . at ( 0 )?. email ?? '' )) }
currency = { policy ?. outputCurrency }
/>
</ OfflineWithFeedback >
)) }
</>
Adding New Workflows
Create new approval workflows for different teams or scenarios:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
const addApprovalAction = useCallback (() => {
setApprovalWorkflow ({
... INITIAL_APPROVAL_WORKFLOW ,
availableMembers ,
usedApproverEmails ,
});
if ( ! isControlPolicy ( policy )) {
Navigation . navigate (
ROUTES . WORKSPACE_UPGRADE . getRoute (
route . params . policyID ,
CONST . UPGRADE_FEATURE_INTRO_MAPPING . approvals . alias ,
ROUTES . WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM . getRoute ( route . params . policyID ),
),
);
return ;
}
Navigation . navigate ( ROUTES . WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM . getRoute ( route . params . policyID ));
}, [ policy , route . params . policyID , availableMembers , usedApproverEmails ]);
Advanced approval workflows require a Control Plan workspace. Free workspaces are limited to basic approval modes.
Workflow Features
Searching Workflows
For workspaces with many workflows, use the search functionality:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
const filterWorkflow = ( workflow : ApprovalWorkflow , searchInput : string ) => {
const searchableTexts : string [] = [];
if ( workflow . isDefault ) {
searchableTexts . push ( everyoneText );
} else {
for ( const member of workflow . members ) {
searchableTexts . push ( member . displayName );
searchableTexts . push ( Str . removeSMSDomain ( member . displayName ));
searchableTexts . push ( member . email );
searchableTexts . push ( Str . removeSMSDomain ( member . email ));
}
}
for ( const approver of workflow . approvers ) {
searchableTexts . push ( approver . displayName );
searchableTexts . push ( Str . removeSMSDomain ( approver . displayName ));
searchableTexts . push ( approver . email );
searchableTexts . push ( Str . removeSMSDomain ( approver . email ));
}
return tokenizedSearch ([ workflow ], searchInput , () => searchableTexts ). length > 0 ;
};
const [ workflowSearchInput , setWorkflowSearchInput , searchFilteredWorkflows ] = useSearchResults ( filteredApprovalWorkflows , filterWorkflow );
Default Workflows
Every workspace has a default workflow that applies when no specific workflow matches:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
const filteredApprovalWorkflows =
policy ?. approvalMode === CONST . POLICY . APPROVAL_MODE . ADVANCED || policy ?. approvalMode === CONST . POLICY . APPROVAL_MODE . DYNAMICEXTERNAL
? approvalWorkflows
: approvalWorkflows . filter (( workflow ) => workflow . isDefault );
Approval Process
Report Approval States
Reports move through various approval states:
Submitted : Report is waiting for first approval
Awaiting Approval : Report is with an approver
Processing : Report is moving through approval chain
Approved : All approvals complete
Rejected : Report was rejected and returned to submitter
Next Steps
New Expensify automatically generates next steps to guide users:
// From src/libs/actions/Report/index.ts
const optimisticNextStep = buildOptimisticNextStep ({
report: optimisticReportData ,
predictedNextStatus: CONST . REPORT . STATUS_NUM . OPEN ,
policy ,
currentUserAccountIDParam: accountID ,
currentUserEmailParam: email ?? '' ,
hasViolations: hasViolationsParam ,
isASAPSubmitBetaEnabled ,
});
if ( optimisticNextStep ) {
optimisticReportData . nextStep = optimisticNextStep ;
}
Integration with Payments
Configuring Payment Settings
Approval workflows integrate with payment settings:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
{
title : translate ( 'workflowsPage.makeOrTrackPaymentsTitle' ),
subtitle : translate ( 'workflowsPage.makeOrTrackPaymentsDescription' ),
switchAccessibilityLabel : translate ( 'workflowsPage.makeOrTrackPaymentsDescription' ),
onToggle : ( isEnabled : boolean ) => {
let newReimbursementChoice ;
if ( ! isEnabled ) {
newReimbursementChoice = CONST . POLICY . REIMBURSEMENT_CHOICES . REIMBURSEMENT_NO ;
} else if ( !! policy ?. achAccount && ! isCurrencySupportedForDirectReimbursement (( policy ?. outputCurrency ?? '' ) as CurrencyType )) {
newReimbursementChoice = CONST . POLICY . REIMBURSEMENT_CHOICES . REIMBURSEMENT_MANUAL ;
} else {
newReimbursementChoice = CONST . POLICY . REIMBURSEMENT_CHOICES . REIMBURSEMENT_YES ;
}
const newReimburserEmail = policy ?. achAccount ?. reimburser ?? policy ?. owner ;
setWorkspaceReimbursement ({
policyID: route . params . policyID ,
reimbursementChoice: newReimbursementChoice ,
reimburserEmail: newReimburserEmail ?? '' ,
bankAccountID: policy ?. achAccount ?. bankAccountID ,
accountNumber: policy ?. achAccount ?. accountNumber ,
addressName: policy ?. achAccount ?. addressName ,
bankName: policy ?. achAccount ?. bankName ,
state: policy ?. achAccount ?. state ,
});
},
isEndOptionRow : true ,
isActive : policy ?. reimbursementChoice !== CONST . POLICY . REIMBURSEMENT_CHOICES . REIMBURSEMENT_NO ,
}
Authorized Payer
Specify who can approve payments for the workspace:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
const displayNameForAuthorizedPayer = useMemo (
() => getDisplayNameOrDefault ( getPersonalDetailByEmail ( policy ?. achAccount ?. reimburser ?? '' ), policy ?. achAccount ?. reimburser ),
[ policy ?. achAccount ?. reimburser ],
);
< MenuItemWithTopDescription
title = { displayNameForAuthorizedPayer ?? '' }
titleStyle = { styles . textNormalThemeText }
descriptionTextStyle = { styles . textLabelSupportingNormal }
description = { translate ( 'workflowsPayerPage.payer' ) }
onPress = { () => Navigation . navigate ( ROUTES . WORKSPACE_WORKFLOWS_PAYER . getRoute ( route . params . policyID )) }
shouldShowRightIcon
/>
Best Practices
Start with basic approval workflows and only add complexity when needed. Complex workflows can slow down the approval process.
Make sure each workflow has clearly assigned approvers who are available and responsive.
Use approval limits to automatically escalate high-value expenses to senior approvers.
Track how long approvals take and adjust workflows to reduce bottlenecks.
Notify team members when approval workflows change so they know what to expect.
Troubleshooting
Approval Stuck If an approval is stuck:
Verify the approver has access to the workspace
Check if the approver’s account is active
Ensure no policy violations are blocking approval
Contact workspace admin if needed
Dynamic External Workflow Issues If you see issues with external approval integrations:
Verify the integration is properly configured
Check connection to external system
Review integration logs
Contact your account manager for assistance
Reports Overview Learn about expense reports
Creating Reports Create and manage reports
Workspace Settings Configure workspace settings