Rules and workflows in New Expensify automate expense management, enforce policies, and streamline approval processes. Set up everything from simple approval chains to complex multi-level workflows with conditional rules.
Approval Workflows
Workflows define how expenses move from submission to reimbursement.
Approval Modes
New Expensify supports three approval modes:
Optional Approval No approval required—expenses are immediately available for reimbursement. approvalMode : CONST . POLICY . APPROVAL_MODE . OPTIONAL
Best for:
Personal workspaces
Small teams with high trust
Non-reimbursable expense tracking
Basic Approval Single-level approval by a designated approver. approvalMode : CONST . POLICY . APPROVAL_MODE . BASIC
Configuration:
Set default approver for all expenses
Optional: Different approvers per member
Best for:
Growing teams
Simple hierarchies
Department-level approval
Advanced Approval Multi-level workflows with conditional routing. approvalMode : CONST . POLICY . APPROVAL_MODE . ADVANCED
Features:
Multiple approval levels
Member-specific workflows
Amount-based routing
Custom approval chains
Best for:
Enterprise organizations
Complex org structures
Compliance requirements
Creating Approval Workflows
Navigate to Workflows
Go to Workspace Settings > Workflows .
Enable Advanced Mode
Click Enable Advanced Workflows (if not already enabled).
Add Workflow
Click Add Workflow and select members this workflow applies to.
Configure Approvers
Add approvers in order. Expenses will route through each level.
Set Conditions
Add amount thresholds or other conditions for routing.
Workflow Creation Code
// From src/libs/actions/Workflow.ts
function createApprovalWorkflow ({
approvalWorkflow ,
policy ,
addExpenseApprovalsTaskReport ,
} : CreateApprovalWorkflowParams ) {
if ( ! policy ) {
return ;
}
const previousEmployeeList = Object . fromEntries (
Object . entries ( policy . employeeList ?? {}). map (([ key , value ]) => [
key ,
{ ... value , pendingAction: null },
])
);
const updatedEmployees = convertApprovalWorkflowToPolicyEmployees ({
previousEmployeeList ,
approvalWorkflow ,
type: CONST . APPROVAL_WORKFLOW . TYPE . CREATE ,
});
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . SET ,
key: ONYXKEYS . APPROVAL_WORKFLOW ,
value: null ,
},
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policy . id } ` ,
value: {
employeeList: updatedEmployees ,
approvalMode: CONST . POLICY . APPROVAL_MODE . ADVANCED ,
},
},
];
const parameters : CreateWorkspaceApprovalParams = {
policyID: policy . id ,
employees: JSON . stringify ( Object . values ( updatedEmployees )),
};
API . write (
WRITE_COMMANDS . CREATE_WORKSPACE_APPROVAL ,
parameters ,
{ optimisticData , failureData , successData }
);
}
Workflow Structure
// Approval workflow structure from src/types/onyx/ApprovalWorkflow.ts
interface ApprovalWorkflow {
approvers : Approver [];
isDefault : boolean ;
members : Member [];
}
interface Approver {
email : string ;
displayName : string ;
avatarURL ?: string ;
accountID : number ;
approvalLimit ?: number ; // Amount threshold
forwardsTo ?: string ; // Escalation
}
interface Member {
email : string ;
displayName : string ;
avatarURL ?: string ;
accountID : number ;
}
Default workflow : Applies to all members not assigned to a specific workflow. Every workspace must have a default workflow.
Setting Approvers
Configure who approves expenses:
// Set approval workflow approver from src/libs/actions/Workflow.ts
function setApprovalWorkflowApprover ({
approver ,
approverIndex ,
currentApprovalWorkflow ,
policy ,
personalDetailsByEmail ,
} : SetApprovalWorkflowApproverParams ) {
const updatedApprovers = [
... ( currentApprovalWorkflow ?. approvers ?? []),
];
if ( approverIndex < updatedApprovers . length ) {
updatedApprovers [ approverIndex ] = approver ;
} else {
updatedApprovers . push ( approver );
}
// Calculate derived approvers
const calculatedApprovers = calculateApprovers (
currentApprovalWorkflow ?. members ?? [],
updatedApprovers ,
personalDetailsByEmail ,
policy ,
);
Onyx . merge ( ONYXKEYS . APPROVAL_WORKFLOW , {
approvers: updatedApprovers ,
calculatedApprovers ,
});
}
Auto-Reporting
Automatically create and submit expense reports on a schedule:
Configuration
Enable Auto-Reporting
Go to Workflows and toggle Auto-Reporting .
Set Frequency
Choose:
Manual : Submit reports manually (default)
Immediate : Submit after each expense
Daily : Every business day
Weekly : End of week
Monthly : End of month
Configure Offset
For monthly reporting, set the day of month to submit (e.g., 25th for month-end closing).
// Set auto-reporting from src/libs/actions/Policy/Policy.ts
function setWorkspaceAutoReportingFrequency (
policyID : string ,
frequency : ValueOf < typeof CONST . POLICY . AUTO_REPORTING_FREQUENCIES >,
) {
const policy = allPolicies [ ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ];
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
autoReporting: true ,
autoReportingFrequency: frequency ,
pendingFields: {
autoReportingFrequency: CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ,
},
},
},
];
const parameters : SetWorkspaceAutoReportingFrequencyParams = {
policyID ,
frequency ,
};
API . write (
WRITE_COMMANDS . SET_WORKSPACE_AUTO_REPORTING_FREQUENCY ,
parameters ,
{ optimisticData , successData , failureData }
);
}
Monthly Offset
For monthly auto-reporting, configure when reports are submitted:
function setWorkspaceAutoReportingMonthlyOffset (
policyID : string ,
autoReportingOffset : string ,
) {
const parameters : SetWorkspaceAutoReportingMonthlyOffsetParams = {
policyID ,
autoReportingOffset ,
};
API . write (
WRITE_COMMANDS . SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET ,
parameters ,
{ optimisticData , failureData }
);
}
Best practice : Set monthly offset to 2-3 days before month-end to allow time for any last-minute expenses and approvals.
Expense Rules
Enforce policies automatically with expense rules:
Receipt Requirements
Make receipts mandatory above a certain amount:
// From src/pages/workspace/rules/RulesReceiptRequiredAmountPage.tsx
function setReceiptRequiredAmount (
policyID : string ,
amount : number ,
) {
const policy = allPolicies [ ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ];
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
requiresReceipt: true ,
receiptRequiredAmount: amount ,
},
},
];
}
Example rule : Require receipts for all expenses over $75.
Expense Limits
Set maximum expense amounts:
Per Expense
Approval Limit
Age Limit
Maximum amount for a single expense: function setMaxExpenseAmount (
policyID : string ,
maxExpenseAmount : number ,
) {
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
maxExpenseAmount ,
},
},
];
}
Auto-approve expenses under threshold: function setPolicyAutomaticApprovalLimit (
policyID : string ,
limit : number ,
) {
const parameters : SetPolicyAutomaticApprovalLimitParams = {
policyID ,
limit ,
};
API . write (
WRITE_COMMANDS . SET_POLICY_AUTOMATIC_APPROVAL_LIMIT ,
parameters ,
{ optimisticData , failureData }
);
}
Reject expenses older than a certain date: function setMaxExpenseAge (
policyID : string ,
maxExpenseAge : number , // days
) {
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
maxExpenseAge ,
},
},
];
}
Example : Reject expenses older than 90 days.
Prohibited Expenses
Block specific expense types:
function setPolicyProhibitedExpenses (
policyID : string ,
prohibitedExpenses : ProhibitedExpenses ,
) {
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
prohibitedExpenses: {
... prohibitedExpenses ,
pendingAction: CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE ,
},
},
},
];
const parameters : SetPolicyProhibitedExpensesParams = {
policyID ,
prohibitedExpenses: JSON . stringify ( prohibitedExpenses ),
};
API . write (
WRITE_COMMANDS . SET_POLICY_PROHIBITED_EXPENSES ,
parameters ,
{ optimisticData , failureData }
);
}
Common prohibitions:
Personal expenses
Alcohol (for certain policies)
Entertainment
Gifts above a threshold
Merchant Rules
Automate expense categorization based on merchant:
// From src/libs/actions/Policy/Rules.ts
function setPolicyCodingRule (
policyID : string ,
form : MerchantRuleForm ,
policy : Policy | undefined ,
ruleID ?: string ,
) {
const targetRuleID = ruleID ?? NumberUtils . rand64 ();
const operator = form . matchType ?? CONST . SEARCH . SYNTAX_OPERATORS . CONTAINS ;
const ruleForOnyx = {
ruleID: targetRuleID ,
filters: {
left: 'merchant' ,
operator ,
right: form . merchantToMatch ,
},
merchant: form . merchant || null ,
category: form . category || null ,
tag: form . tag || null ,
comment: convertCommentToHTML ( form . comment ),
reimbursable: form . reimbursable ?? null ,
billable: form . billable ?? null ,
pendingAction: isEditing
? CONST . RED_BRICK_ROAD_PENDING_ACTION . UPDATE
: CONST . RED_BRICK_ROAD_PENDING_ACTION . ADD ,
};
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
rules: {
codingRules: {
[targetRuleID]: ruleForOnyx ,
},
},
},
},
];
const parameters : SetPolicyCodingRuleParams = {
policyID ,
ruleJSON: JSON . stringify ( ruleFieldsForAPI ),
ruleID: targetRuleID ,
shouldUpdateMatchingTransactions ,
};
API . write (
WRITE_COMMANDS . SET_POLICY_CODING_RULE ,
parameters ,
{ optimisticData , successData , failureData }
);
}
Example rules:
Expenses from “Starbucks” → automatically categorize as “Meals”
Expenses from “United Airlines” → tag as “Travel” with “Client Meetings” tag
Expenses from “AWS” → categorize as “Software”, make billable
Merchant rules support:
Exact match : Merchant name must match exactly
Contains : Merchant name contains the text
Starts with : Merchant name starts with the text
Regex : Advanced pattern matching
Reimbursement Settings
Configure how expenses are reimbursed:
Reimbursement Methods
Reimburse outside the app (cash, check, etc.): reimbursementChoice : CONST . POLICY . REIMBURSEMENT_CHOICES . REIMBURSEMENT_MANUAL
ACH/bank transfer directly to employee: reimbursementChoice : CONST . POLICY . REIMBURSEMENT_CHOICES . REIMBURSEMENT_YES
Requires bank account connection.
// Set reimbursement method from src/libs/actions/Policy/Policy.ts
function setWorkspaceReimbursement (
policyID : string ,
reimbursementChoice : ValueOf < typeof CONST . POLICY . REIMBURSEMENT_CHOICES >,
reimburserEmail : string ,
) {
const policy = allPolicies [ ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ];
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
reimbursementChoice ,
achAccount: {
reimburser: reimburserEmail ,
},
},
},
];
const parameters : SetWorkspaceReimbursementParams = {
policyID ,
reimbursementChoice ,
reimburserEmail ,
};
API . write (
WRITE_COMMANDS . SET_WORKSPACE_REIMBURSEMENT ,
parameters ,
{ optimisticData , successData , failureData }
);
}
Auto-Pay Rules
Automatically reimburse approved reports:
function setAutoPayReportsUnder (
policyID : string ,
amount : number ,
) {
// Auto-pay reports under specified amount
const optimisticData : OnyxUpdate [] = [
{
onyxMethod: Onyx . METHOD . MERGE ,
key: ` ${ ONYXKEYS . COLLECTION . POLICY }${ policyID } ` ,
value: {
autoPayReportsUnder: amount ,
},
},
];
}
Workflow Page Implementation
Here’s how the workflows page works:
// From src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
function WorkspaceWorkflowsPage ({ policy , route } : WorkspaceWorkflowsPageProps ) {
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 ;
const addApprovalAction = useCallback (() => {
setApprovalWorkflow ({
... INITIAL_APPROVAL_WORKFLOW ,
availableMembers ,
usedApproverEmails ,
});
Navigation . navigate (
ROUTES . WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM . getRoute ( policyID )
);
}, [ policy , policyID , availableMembers , usedApproverEmails ]);
return (
< WorkspacePageWithSections >
< ApprovalWorkflowSection
approvalWorkflows = { filteredApprovalWorkflows }
onAddWorkflow = { addApprovalAction }
/>
< ExpenseReportRulesSection policy = { policy } />
</ WorkspacePageWithSections >
);
}
Best Practices
Begin with basic approval (single approver) and add advanced workflows only when needed. Complex workflows can slow down approvals.
Auto-approval limits should be high enough to be useful but low enough to maintain control. Common range: 50 − 50- 50 − 500.
Use Auto-Reporting Wisely
Monthly auto-reporting works well for most teams. Daily is usually too frequent and creates noise.
Maintain a policy document explaining all rules and workflows. Share it during onboarding.
Audit your workflows quarterly. Remove outdated rules and optimize approval chains based on actual usage.
Create rules for your most common merchants. This saves time and improves categorization consistency.
Troubleshooting
Check if:
Approver has workspace access
Approver email is correct
Workflow isn’t circular (A approves for B, B approves for A)
Expense meets approval criteria
Auto-Reporting Not Working
Verify:
Feature is enabled for your plan
Frequency is set correctly
User has submitted expenses in the period
No holds or violations blocking submission
Ensure:
Rule is enabled
Merchant name matches exactly (check for typos/spacing)
Rule order is correct (rules apply in sequence)
Categories/tags in rule are still enabled
Next Steps
Approval Workflows Deep dive into report approval processes
Workspace Overview Back to workspace features
Categories & Tags Review expense organization
Integrations Connect accounting systems