Skip to content

[Due for payment 2026-10-23] [$175] Workflows - Members submitting to a removed approver are hidden and keep the workspace on Advanced Approval #102234

Closed

Description

@melvin-bot

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).

  1. Go to Workspace > Workflows.
  2. 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

  1. Hidden member: convertPolicyEmployeesToApprovalWorkflows in WorkflowUtils.ts skips any employee whose submitsTo isn't in employeeList.
  2. Stuck on Advanced: removeApprovalWorkflow and updateApprovalWorkflow in Workflow.ts check for multiple workflows across the full employee list. That list includes the hidden member, so one hidden member keeps approvalMode on ADVANCED.
  3. How orphans are created: removeUsers in WorkspaceMembersPage.tsx reassigns the removed approver's submitters, but it skips this step when personalDetails[accountID] has no login. removeMembers still runs, so the member is removed and submitsTo still 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

  1. Always reassign submitters when an approver is removed. In both member pages, build removedApprover as personalDetails?.[accountID] ?? {accountID, login}, using the employeeList key as the login. Fall back to policy.owner when ownerDetails.login is empty.
  2. Show hidden members. In convertPolicyEmployeesToApprovalWorkflows, put members whose submitsTo isn't a current, non-deleted employee in the default workflow instead of dropping them.
  3. Fix hidden members whenever a workflow is saved. In removeApprovalWorkflow and updateApprovalWorkflow, add those members to updatedEmployees with submitsTo set to the default approver. Do this before checking for multiple workflows. The fix then reaches the backend, and approval mode can reset to Basic.
  4. Tests: cover a submitsTo that 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 OwnerCurrent Issue Owner: @mallenexpensify
Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 24, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 24, 2026
  3. melvin-bot commented on Sep 24, 2026

    @melvin-bot
    Author

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @ikevin127 (External)

  4. changed the title [-]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[/+] on Sep 24, 2026
  5. melvin-bot commented on Sep 24, 2026

    @melvin-bot
    Author

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  6. trjExpensify commented on Sep 24, 2026

    @trjExpensify
    Contributor

    @MelvinBot

    Show hidden members. In convertPolicyEmployeesToApprovalWorkflows, put members whose submitsTo isn't a current, non-deleted employee in the default workflow instead of dropping them.

    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.

  7. MelvinBot commented on Sep 24, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    I confirmed all three causes described in the issue body in the App code.

    1. How orphans are created. Both member pages look up the removed approver in personalDetails. If that entry has no login, or the owner's entry has no login, they skip the step that reassigns submitters. They still call removeMembers, and that call sends only {emailList, policyID}. So other members keep submitsTo pointing 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.
    2. Hidden member. convertPolicyEmployeesToApprovalWorkflows skips any member whose submitsTo isn't in employeeList. The default workflow still says "Everyone", so the admin has no way to see or fix these members.
    3. Stuck on Advanced. removeApprovalWorkflow and updateApprovalWorkflow keep ADVANCED when any employee's submitsTo is not the default approver. The hidden member matches, but it is never part of the request, so its submitsTo is never corrected.
    Code references

    What changes do you think we should make in order to solve the problem?

    1. Always reassign on approver removal. In both member pages, use the employeeList email (login / memberLogin) as removedApprover.login instead of requiring a personal-details entry. Use ownerDetails.login ?? policy.owner for the owner. On the details page, also guard on !memberLogin.

    2. Show orphaned members. In convertPolicyEmployeesToApprovalWorkflows, treat a submitsTo that isn't in employeeList as 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.

    3. Fix orphans on save. Add a small helper that removeApprovalWorkflow and updateApprovalWorkflow both call before the multiple-workflow check. It adds each orphaned member to updatedEmployees with submitsTo set to the default approver. It should:

      • skip policies where isAnyHRReadOnlyWorkflowMode(policy) is true;
      • skip members that are themselves pending DELETE;
      • count a member as orphaned only when its approver is missing from employeeList, not when the approver is pending DELETE (those approvers are shown struck through on purpose).

      Also clear pendingFields in removeApprovalWorkflow success data.

    4. Tests.

      • Update WorkflowUtilsTest "Should not include users that submit to non-employee user". It should now expect the member in the default workflow.
      • Keep the HR manager-mode non-member test unchanged.
      • Add WorkflowTest cases: an orphan ends up in BASIC mode with submitsTo set to the default approver; HR mode leaves orphans alone.
      • Add a WorkspaceMembersTest case: removing an approver with no personal details reassigns their submitters.
    Notes and risks
    • I couldn't verify the backend side from App. The client only decides the optimistic approvalMode. Before merging, an internal engineer should confirm two things: the server resets the mode the same way, and it accepts an explicit submitsTo for employees outside the edited workflow.
    • After step 2, both member pages call updateApprovalWorkflow for every workflow. So removing any approver will also fix existing orphans. That is likely what we want, but it needs a test.
    • Open PR Skip members pending deletion when building approval workflows rewrites the same loop, so expect merge conflicts.
    • The same bug has a deeper form: a default approver's forwardsTo can 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.

  8. MelvinBot commented on Sep 24, 2026

    @MelvinBot
    Contributor

    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 submitsTo behind the admin's back.

    Revised changes

    1. Prevent new orphans (unchanged). When an approver is removed, both member pages always reassign that approver's submitters. They use the employeeList email and policy.owner as fallbacks.
    2. Show the broken workflow as it is configured. In convertPolicyEmployeesToApprovalWorkflows, stop skipping members whose submitsTo isn't on the workspace. Group them under that approver, as today's fallback approver builder already does. Add an isNotWorkspaceMember flag to the approver. Keep the HR-mode exception.
    3. Show an error.
      • The workflow card shows an error row under the flagged approver, for example: "This approver is no longer a workspace member. Choose a new approver or move these members to another workflow."
      • Workspace > Workflows gets a red dot in Advanced mode.
      • The editor blocks Save until the admin replaces the approver.
    4. Let the admin fix it with existing flows.
      • Change approver: the picker already lists only members. The update then rewrites each member's submitsTo.
      • Delete the workflow: members fall back to the default approver. When no custom workflows remain, the workspace drops to Basic on its own.
    5. Payload guard (required). In 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

    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.

  9. ikevin127 commented on Sep 25, 2026

    @ikevin127
    Contributor
    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 / removeUser skip updateWorkflowDataOnApproverRemoval when personalDetails[accountID]?.login is missing, but still call removeMembers. For these members accountID is always undefined, because getMemberAccountIDsForWorkspace skips anyone without a personal-details login (PolicyUtils.ts:909-912). So the step that reassigns submitters can never run for them.
    • convertPolicyEmployeesToApprovalWorkflows drops members whose submitsTo isn't in employeeList (WorkflowUtils.ts:205).
    • The hasMoreThanOneWorkflow / hasMultipleWorkflows checks in Workflow.ts scan 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 submitsTo behind their back. I traced the delete path: once the orphan workflow is visible and deleted, its members go to submitsTo: '', 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 in convertApprovalWorkflowToPolicyEmployees sees previousEmployeeList[nonMember] as undefined, marks forwardsTo as 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. findFirstNonExpensifyApprover only runs when employees[submitsTo] exists. So if the orphaned submitsTo is an Expensify team email and shouldFilterOutExpensifyTeam is true, the filter at WorkflowUtils.ts:226 leaves approvers empty. That workflow then renders with no approver, gets silently dropped by updateWorkflowDataOnApproverRemoval, and throws in convertApprovalWorkflowToPolicyEmployees (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. updateWorkflowDataOnApproverRemoval only reads removedApprover.login, but the param is typed as PersonalDetails, which requires accountID. accountID is undefined for exactly these members, so building {accountID, login} means filling in a placeholder ID. Changing the helper to take removedApproverEmail: string, with ownerDetails.login ?? policy.owner for 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: submitsTo isn't in employeeList and this isn't HR advanced mode. Put it in PolicyUtils once and reuse it. Also consider using it in getPolicyBrickRoadIndicatorStatus. 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 getSubmitToEmail in 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) and Should include members with orphaned submitsTo in availableMembers (WorkflowUtilsTest.ts:578). Add WorkflowTest cases 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

  10. melvin-bot commented on Sep 25, 2026

    @melvin-bot
    Author

    Triggered auto assignment to @tylerkaraszewski, see https://stackoverflow.com/c/expensify/questions/7972 for more details.

  11. 38 remaining items

  12. m-natarajan commented on Oct 21, 2026

    @m-natarajan

    @ikevin127 This is regarding the Regression test proposal
    Test: 1, Step 5 fails. New workflow created (Approver updated and the workflow remains), Can you please check the video?

    Recording.3724.mp4
  13. ikevin127 commented on Oct 21, 2026

    @ikevin127
    Contributor

    @m-natarajan thanks for the video! Could you check if this account has the multipleApprovers beta 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-based Asdf → App Test workflow 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:

    This is an existing gap, not a regression from #102815: approver removal from the Members page never handled rule-based workflows.

  14. melvin-bot commented on Oct 22, 2026

    @melvin-bot
    Author

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  15. melvin-bot commented on Oct 22, 2026

    @melvin-bot
    Author

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  16. ikevin127 commented on Oct 23, 2026

    @ikevin127
    Contributor

    ⌛ Still pending a response from @tylerkaraszewski on the payment increase request with this addition regarding regression talk.

  17. mallenexpensify commented on Oct 26, 2026

    @mallenexpensify
    Contributor

    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

  18. ikevin127 commented on Oct 26, 2026

    @ikevin127
    Contributor

    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.

    image

  19. tylerkaraszewski commented on Oct 26, 2026

    @tylerkaraszewski
    Contributor

    I think we're settled on $175.

  20. quinthar commented on Oct 27, 2026

    @quinthar
    Contributor

    Approved $175 to @ikevin127

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions