Repository navigation
[Due for payment 2026-10-23] [$175] Workflows - Members submitting to a removed approver are hidden and keep the workspace on Advanced Approval #102234
Description
Problem
A member's submitsTo can point to someone who is no longer on the workspace. When that happens, NewDot hides the member on the Workflows page. If the admin then deletes every workflow, the workspace stays on Advanced Approval and the member's reports still go to the removed approver. The member can't submit, and the admin can't fix it in NewDot. A customer hit this and Support had to clean up the data manually.
Steps to reproduce
Precondition: a member's submitsTo is an email that isn't in employeeList (see "How orphans are created" below).
- Go to Workspace > Workflows.
- Delete all non-default workflows.
Expected: approval mode resets to Basic, and every member submits to the default approver.
Actual: the member isn't shown in any workflow. Approval mode stays Advanced. Their reports go to the removed approver.
Root cause
- Hidden member:
convertPolicyEmployeesToApprovalWorkflowsin WorkflowUtils.ts skips any employee whosesubmitsToisn't inemployeeList. - Stuck on Advanced:
removeApprovalWorkflowandupdateApprovalWorkflowin Workflow.ts check for multiple workflows across the full employee list. That list includes the hidden member, so one hidden member keepsapprovalModeon ADVANCED. - How orphans are created:
removeUsersin WorkspaceMembersPage.tsx reassigns the removed approver's submitters, but it skips this step whenpersonalDetails[accountID]has no login.removeMembersstill runs, so the member is removed andsubmitsTostill points at them. WorkspaceMemberDetailsPage.tsx has the same check. The members table already uses a fallback identity when personal details are missing, but the removal code doesn't.
Proposed solution
- Always reassign submitters when an approver is removed. In both member pages, build
removedApproveraspersonalDetails?.[accountID] ?? {accountID, login}, using theemployeeListkey as the login. Fall back topolicy.ownerwhenownerDetails.loginis empty. - Show hidden members. In
convertPolicyEmployeesToApprovalWorkflows, put members whosesubmitsToisn't a current, non-deleted employee in the default workflow instead of dropping them. - Fix hidden members whenever a workflow is saved. In
removeApprovalWorkflowandupdateApprovalWorkflow, add those members toupdatedEmployeeswithsubmitsToset to the default approver. Do this before checking for multiple workflows. The fix then reaches the backend, and approval mode can reset to Basic. - Tests: cover a
submitsTothat points at a non-member in WorkflowUtils and the Workflow actions. Also test removing an approver who has no personal details loaded.
Related: Auto - Workspace - Approver is not replaced by owner after default approver leaves, [Exploratory] Control Workspace - Both approvers remove in OD still remain in Workflows ND
Reported in Slack.
Issue Owner
Current Issue Owner: @mallenexpensifyUpwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022107974207489229976
- Upwork Job ID: 2107974207489229976
- Last Price Increase: 2026-10-07
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
melvin-bot commentedon Sep 24, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @ikevin127 (
External)[-]Workflows - Members submitting to a removed approver are hidden and keep the workspace on Advanced Approval[/-][+][$175] Workflows - Members submitting to a removed approver are hidden and keep the workspace on Advanced Approval[/+]melvin-bot commentedon Sep 24, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
trjExpensify commentedon Sep 24, 2026
@MelvinBot
I'm not really sure I'm feeling this part of your proposal. If the member is submitting to someone not on the workspace through some crusty flow that let them get in that spot, it's misleading to display them in the default workflow when in fact that's not their workflow and the source of what's broken.
If anything, we should show who they are actually configured to submit to, and highlight that with an error to fix or something. @JmillsExpensify curious for your take here btw. This customer got into a real weird spot with submitting to people outside of the workspace, and because those broken workflows weren't shown in NewDot - they had no means to fix it in the product.
MelvinBot commentedon Sep 24, 2026
Proposal
What is the root cause of that problem?
I confirmed all three causes described in the issue body in the App code.
personalDetails. If that entry has nologin, or the owner's entry has nologin, they skip the step that reassigns submitters. They still callremoveMembers, and that call sends only{emailList, policyID}. So other members keepsubmitsTopointing at the removed email. This became reachable when Show workspace members with missing personal details instead of dropping them made these members selectable in the table.convertPolicyEmployeesToApprovalWorkflowsskips any member whosesubmitsToisn't inemployeeList. The default workflow still says "Everyone", so the admin has no way to see or fix these members.removeApprovalWorkflowandupdateApprovalWorkflowkeep ADVANCED when any employee'ssubmitsTois not the default approver. The hidden member matches, but it is never part of the request, so itssubmitsTois never corrected.Code references
removedApproveris always undefined for them.updateWorkflowDataOnApproverRemovalonly uses the email stringsremovedApprover.loginandownerDetails.login(WorkflowUtils.ts:458-463). TheemployeeListkey andpolicy.ownerare enough.removeMembersnever rewrites other members'submitsTo/forwardsTo(Member.ts:412-439).What changes do you think we should make in order to solve the problem?
Always reassign on approver removal. In both member pages, use the
employeeListemail (login/memberLogin) asremovedApprover.logininstead of requiring a personal-details entry. UseownerDetails.login ?? policy.ownerfor the owner. On the details page, also guard on!memberLogin.Show orphaned members. In
convertPolicyEmployeesToApprovalWorkflows, treat asubmitsTothat isn't inemployeeListas the default approver, so the member joins the default workflow. Keep the HR manager-mode exception (hrAdvancedModeFinalApproverEmail). Apply the same rule to the beta rules-based builder for members not covered by a rule.Fix orphans on save. Add a small helper that
removeApprovalWorkflowandupdateApprovalWorkflowboth call before the multiple-workflow check. It adds each orphaned member toupdatedEmployeeswithsubmitsToset to the default approver. It should:isAnyHRReadOnlyWorkflowMode(policy)is true;employeeList, not when the approver is pending DELETE (those approvers are shown struck through on purpose).Also clear
pendingFieldsinremoveApprovalWorkflowsuccess data.Tests.
WorkflowUtilsTest"Should not include users that submit to non-employee user". It should now expect the member in the default workflow.WorkflowTestcases: an orphan ends up in BASIC mode withsubmitsToset to the default approver; HR mode leaves orphans alone.WorkspaceMembersTestcase: removing an approver with no personal details reassigns their submitters.Notes and risks
approvalMode. Before merging, an internal engineer should confirm two things: the server resets the mode the same way, and it accepts an explicitsubmitsTofor employees outside the edited workflow.updateApprovalWorkflowfor every workflow. So removing any approver will also fix existing orphans. That is likely what we want, but it needs a test.forwardsTocan point at a non-member. That can stay out of scope.What alternative solutions did you explore? (Optional)
Only fix step 1. That stops new orphans, but workspaces that already have them stay stuck.
Possibly related: Auto - Workspace - Approver is not replaced by owner after default approver leaves, [Exploratory] Control Workspace - Both approvers remove in OD still remain in Workflows ND, Broken workflow disappears
Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/102234#issuecomment-5823680566)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
MelvinBot commentedon Sep 24, 2026
Agreed. Putting these members in the default workflow hides the real problem. Here's the revised proposal: show each broken workflow with its real approver, flag it with an error, and let the admin fix it with the existing edit and delete flows. This also replaces my earlier step 3. We shouldn't quietly rewrite
submitsTobehind the admin's back.Revised changes
employeeListemail andpolicy.owneras fallbacks.convertPolicyEmployeesToApprovalWorkflows, stop skipping members whosesubmitsToisn't on the workspace. Group them under that approver, as today's fallback approver builder already does. Add anisNotWorkspaceMemberflag to the approver. Keep the HR-mode exception.submitsTo.convertApprovalWorkflowToPolicyEmployees, skip flagged approvers. Otherwise every save, delete, and approver removal on the Members pages would send a fake employee entry for the non-member email.Code references and risks
errorFields.reimbursertoday.updateApprovalWorkflowfor every workflow (WorkspaceMembersPage.tsx:252-265).Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/102234#issuecomment-5823831441)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
ikevin127 commentedon Sep 25, 2026
Detailed proposals review
Proposal 1 (MelvinBot)
Root cause ✅ All three causes check out against current
main. None of the files it references have changed since the commit it linked.removeUsers/removeUserskipupdateWorkflowDataOnApproverRemovalwhenpersonalDetails[accountID]?.loginis missing, but still callremoveMembers. For these membersaccountIDis always undefined, becausegetMemberAccountIDsForWorkspaceskips anyone without a personal-details login (PolicyUtils.ts:909-912). So the step that reassigns submitters can never run for them.convertPolicyEmployeesToApprovalWorkflowsdrops members whosesubmitsToisn't inemployeeList(WorkflowUtils.ts:205).hasMoreThanOneWorkflow/hasMultipleWorkflowschecks inWorkflow.tsscan the whole employee list, so a hidden orphan keeps the workspace on ADVANCED.Solution ✅ The revised version follows trjExpensify's direction: show the workflow as it is really configured, flag it, and let the admin fix it with the existing edit and delete flows, without rewriting
submitsTobehind their back. I traced the delete path: once the orphan workflow is visible and deleted, its members go tosubmitsTo: '', the mode check finds no other workflows, and the workspace drops to BASIC. So the "stuck on Advanced" symptom is fixed without special handling. The payload guard in step 5 is a good catch. Without it, the approver loop inconvertApprovalWorkflowToPolicyEmployeesseespreviousEmployeeList[nonMember]as undefined, marksforwardsToas changed, and sends a fake employee entry. That would happen on every save and on every approver removal from the Members pages.Things to address before or during the PR:
🟠 Zero-approver workflow is listed as a risk but has no fix.
findFirstNonExpensifyApproveronly runs whenemployees[submitsTo]exists. So if the orphanedsubmitsTois an Expensify team email andshouldFilterOutExpensifyTeamis true, the filter atWorkflowUtils.ts:226leavesapproversempty. That workflow then renders with no approver, gets silently dropped byupdateWorkflowDataOnApproverRemoval, and throws inconvertApprovalWorkflowToPolicyEmployees(WorkflowUtils.ts:376). I'd suggest not filtering out the flagged approver, so the admin can still see and fix it.🟡 Beta rules-based builder was left out of the revised step 2. The original proposal applied the same change at
WorkflowUtils.ts:1628, but the revised one only links to it. If the beta is on, orphans stay hidden there, and so does the new error. Both builders need the same change.🟡 Step 1: narrow the helper params instead of building a fake
PersonalDetails.updateWorkflowDataOnApproverRemovalonly readsremovedApprover.login, but the param is typed asPersonalDetails, which requiresaccountID.accountIDis undefined for exactly these members, so building{accountID, login}means filling in a placeholder ID. Changing the helper to takeremovedApproverEmail: string, withownerDetails.login ?? policy.ownerfor the owner, is cleaner.🟡 Keep the orphan check in one pure helper. The approver flag, the Workflows menu red dot (
getWorkspaceMenuItems.ts:337), and the editor Save block all need the same check:submitsToisn't inemployeeListand this isn't HR advanced mode. Put it inPolicyUtilsonce and reuse it. Also consider using it ingetPolicyBrickRoadIndicatorStatus. Otherwise the admin only sees the red dot after opening the workspace, and the workspace list shows nothing.🟡 The error text depends on backend routing that hasn't been confirmed. The issue says reports go to the removed approver, but
getSubmitToEmailin the client falls back to the default approver (PolicyUtils.ts:2176-2179). The error text has to match what the backend really does. That needs an internal engineer to confirm, along with the product input trjExpensify asked JmillsExpensify for. The new strings also need to go into every locale file.🟢 Tests. Update
Should not include users that submit to non-employee user(WorkflowUtilsTest.ts:337) andShould include members with orphaned submitsTo in availableMembers(WorkflowUtilsTest.ts:578). AddWorkflowTestcases for the delete flow dropping to BASIC and for the payload guard not sending the non-member. Add a Members page case for removing an approver with no personal details.@MelvinBot's proposal + revision LGTM, see Detailed proposals review above for details on why.
It's the only proposal, the root cause is correct and I checked it against the code, and the revised approach matches the direction trjExpensify gave. Step 1 (stop new orphans from being created) is independent and could ship first.
Steps 2 to 5 add new UI: an error row, a red dot, and a blocked Save. Before running the implement command, I'd get a quick 👍 on those from @trjExpensify and @JmillsExpensify, then fold in the 🟠 and 🟡 notes above.
🎀👀🎀 C+ reviewed
melvin-bot commentedon Sep 25, 2026
Triggered auto assignment to @tylerkaraszewski, see https://stackoverflow.com/c/expensify/questions/7972 for more details.
38 remaining items
m-natarajan commentedon Oct 21, 2026
@ikevin127 This is regarding the
Regression test proposalTest: 1, Step 5 fails. New workflow created (Approver updated and the workflow remains), Can you please check the video?
Recording.3724.mp4
ikevin127 commentedon Oct 21, 2026
@m-natarajan thanks for the video! Could you check if this account has the
multipleApproversbeta enabled ? With that beta, workflows are saved as approval rules, and removing an approver from the Members page only updates the legacy workflow data, so the rule-basedAsdf→App Testworkflow stays as its own card.That flow wasn't changed by #102815. Test 1 assumes the beta is OFF, so I updated the regression test precondition, and we can open a separate issue for member removal not cleaning up rule-based workflows.
Why it probably passed earlier during post-merge QA:
multipleApproversbeta, and this regression test run had it on. I can't see QA's active betas, so they should be confirmed.melvin-bot commentedon Oct 22, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Oct 22, 2026
Payment Summary
Resolving PRs:
Show workflows whose approver left the workspace, and stop creating them #102815
Skip null employeeList entries in the Workspaces tab red-dot check #103811
Reviewer: @ikevin127 owed $175 via NewDot
BugZero Checklist (@mallenexpensify)
ikevin127 commentedon Oct 23, 2026
⌛ Still pending a response from @tylerkaraszewski on the payment increase request with this addition regarding regression talk.
mallenexpensify commentedon Oct 26, 2026
Payment Summary
Contributor+: @ikevin127 due $175 via NewDot
Paying $175 cuz that's the standard job price, not taking into account regression because of 'should have caught' post above as well as the regression PR was fixed by @ikevin127 and didn't involve additional payments
ikevin127 commentedon Oct 26, 2026
Note
I messaged Tyler on Slack sometime yesterday (before Matt's payment summary landed on $175) regarding the price increase request - to offer his take when he gets a chance.
In the meantime I went with the $175 summary on thr ND request, but in case Tyler will comment - I wanted to mention this so it doesn't look like I overwritten Matt's decision on the price.
tylerkaraszewski commentedon Oct 26, 2026
I think we're settled on $175.
quinthar commentedon Oct 27, 2026
Approved $175 to @ikevin127