API node deletion orphans the child nodes

I’m building a Dynalist MCP server that lets AI assistants interact with Dynalist via the public API. While implementing node deletion, I discovered that the API’s delete action does not cascade-delete child nodes. To prevent orphaning, I’m having to read the full subtree and recursively delete all descendants bottom-up before deleting the parent.

Steps to reproduce

  1. Using the API (/api/v1/doc/edit), insert a parent node under root with action: "insert".
  2. Insert a child node under that parent with action: "insert".
  3. Delete the parent node with action: "delete" (only specifying the parent’s node_id).
  4. Read the document with /api/v1/doc/read and inspect the nodes array.

Expected result

The parent node and all of its descendants should be removed from the nodes array, consistent with how the web UI handles deletion.

Actual result

The parent node is removed, but the child node remains in the nodes array. No other node references it in its children array, so it is orphaned. The node is unreachable but still present in the document data. Over time, repeated API deletions of nodes with children silently accumulate orphans and inflate the document size.

Environment

N/A. This is a server-side API bug, reproducible via curl from any environment.

Additional information

The third-party Android app QuickDynalist is also affected. It sends a single delete action for the parent node only. The app masks the problem by removing descendants from its local database, so the UI appears correct, but orphans persist on the server.

Another user reported the same underlying issue in the context of bulk deletion: Bulk delete

500 top-level nodes shouldn’t be that common. If you delete a top level item, all child items are removed and it counts as 1 operation.

@shida this response to the above post made it sound like a delete should clean-up all of its children? But it doesn’t seem like that’s fully the case.

A server-side orphan garbage collector would resolve this cleanly. It would retroactively clean up all orphans already accumulated across every API consumer, not just prevent future ones. The alternative of expecting every client to implement recursive bottom-up deletion is fragile and clearly not happening in practice (see QuickDynalist above).

I surveyed every public Dynalist API client I could find on GitHub. Out of 7 repos with delete functionality, 6 are affected by this. They all send a single action: "delete" for the parent node without walking the subtree first. Each link below points to the relevant deletion code:

The only repo that has a workaround is cristip73/dynalist-mcp, which has a collectDescendants function to walk the subtree and delete bottom-up. However, this is behind an include_children flag that defaults to false, so orphaning can still happen if the caller doesn’t opt in.

Looking at the minified web client JS, the web UI doesn’t use the public API’s action: "delete" at all. It uses a diff-based sync protocol where the client’s remove() method recursively detaches all children, adds every descendant to a removed array via make_remove_diff, and sends the full list to the server. The server just applies the diff as-is, meaning it doesn’t cascade-delete on its own, similar to the public API.

The API docs also don’t mention what happens to children when a node is deleted. Every client author reasonably assumed the API would handle this, so at minimum a note in the docs would help. Ideally the server would just cascade-delete (or garbage-collect orphans) so clients don’t have to.

Yeah this is a pretty tough one, part of the reason why I didn’t want to expose an API for a long time (back then). The client internally does a lot of crazy things that are hard to replicate for API users and the server partially hare code with the undocumented client API. Will look into this with the other one later.

1 Like