Environment
- Frappe v16.24.3 (identical flow confirmed on version-16 HEAD 16.32.0)
- Raven co-installed with frappe/gameplan (both auto-provision per-User records)
Summary
When a User is deleted, raven's remove_user_from_raven hook deletes the Raven User record, and RavenUser.after_delete then calls user.save(). That nested save re-enters the complete post-save lifecycle (run_post_save_methods → run_method("on_update") → all on_update doc_events) while the outer document is still mid-deletion. With other apps installing reactive on_update hooks, this composition can make User deletion fail entirely.
Reproduction (frappe v16 + gameplan + raven)
- Create a User — Raven User and GP User Profile records are auto-created by their respective hooks
- Delete the User
- →
frappe.exceptions.LinkExistsError: Cannot delete or cancel because User ... is linked with GP User Profile ...
The GP User Profile was already deleted by gameplan's own on_trash hook earlier in the flow; it is re-created mid-flow (the profile carries a fresh creation timestamp after the failed delete — this is in-flow recreation, not a rollback artifact). Probe-captured chain:
delete_doc.py:165 doc.run_method("on_trash")
→ gameplan delete_user_profile # deletes GP User Profile ✓
→ raven remove_user_from_raven
→ RavenUser.delete()
→ RavenUser.after_delete
→ user.save() # ← re-enters on_update
→ gp_user_profile.py on_user_update
→ create_user_profile(doc) # ← GP User Profile resurrected
delete_doc.py:172 check_if_doc_is_linked # ← now fails: profile exists again
Why this is a lifecycle re-entry issue (not "save is illegal in delete hooks")
Frappe core itself performs nested saves inside User.on_trash (note.save(), invite_doc.save()), so nested saves are an established pattern. The problem is amplification: each nested save() re-enters run_post_save_methods() and fires every installed app's on_update doc_events, letting auto-provisioning hooks in other apps react to a document that is being deleted.
Suggested fix
In RavenUser.after_delete, persist the role removal without re-entering the full save lifecycle — e.g. delete the Has Role child row directly (frappe.db.delete("Has Role", {"role": "Raven User", "parent": self.user, "parenttype": "User"})) + frappe.clear_cache(user=self.user), instead of user.remove_roles(...); user.save().
Verification
Deleting a User that has a Raven User record should succeed with no error; Raven User record, role, and dependent records should all be cleaned up.
Environment
Summary
When a User is deleted, raven's
remove_user_from_ravenhook deletes the Raven User record, andRavenUser.after_deletethen callsuser.save(). That nested save re-enters the complete post-save lifecycle (run_post_save_methods→run_method("on_update")→ allon_updatedoc_events) while the outer document is still mid-deletion. With other apps installing reactiveon_updatehooks, this composition can make User deletion fail entirely.Reproduction (frappe v16 + gameplan + raven)
frappe.exceptions.LinkExistsError: Cannot delete or cancel because User ... is linked with GP User Profile ...The GP User Profile was already deleted by gameplan's own
on_trashhook earlier in the flow; it is re-created mid-flow (the profile carries a freshcreationtimestamp after the failed delete — this is in-flow recreation, not a rollback artifact). Probe-captured chain:Why this is a lifecycle re-entry issue (not "save is illegal in delete hooks")
Frappe core itself performs nested saves inside
User.on_trash(note.save(),invite_doc.save()), so nested saves are an established pattern. The problem is amplification: each nestedsave()re-entersrun_post_save_methods()and fires every installed app'son_updatedoc_events, letting auto-provisioning hooks in other apps react to a document that is being deleted.Suggested fix
In
RavenUser.after_delete, persist the role removal without re-entering the full save lifecycle — e.g. delete theHas Rolechild row directly (frappe.db.delete("Has Role", {"role": "Raven User", "parent": self.user, "parenttype": "User"})) +frappe.clear_cache(user=self.user), instead ofuser.remove_roles(...); user.save().Verification
Deleting a User that has a Raven User record should succeed with no error; Raven User record, role, and dependent records should all be cleaned up.