Fundamentally speaking this sort of thing just boils down to an AST. If you think about what a "multi-stage" image editor is doing, it's just allowing the user to construct an expression that evaluates to the image. In something like Photoshop, this expression cannot have named variables (because it is always evaluated in layer order). In more flexible packages like Flame/Lustre/Flare etc., it's a full AST, with reusable subexpressions and so on. The fact that these things are presented like a layer list or a tree rather than like a programming language is just that - presentation. There's no difference.
So I would say that you can take a lot of guidance here from how compilers do their work, because you're going to want to do exactly the same things that compilers do, especially when you have to do things like accelerate updates. This is why I would recommend against a blind OOP approach, because while the allure is always that "the node can hide its implementation", in practice this is never what you actually want because you end up needing to construct algorithms that operate on entire segments of the AST in order to reorder, collapse, optimize, and partially evaluate it.
Thus you typically want to go with something where you have a generic type field that anyone can look at and test, a reliable way anyone can iterate over children or get the parent, and then some specific data that is relevant to the node. Basically just standard AST construction.
That's my $.02 :)